linkedin Skip to Main Content
Just announced: We now support interviewing in spreadsheets!
Back to blog

How to Build a Photo Editor With React and CSS Filters

Development

CSS is the language used to control the style of a web document in a simple and easy way. It helps makes a web application look more presentable and visually appealing. 

In a world of constant development and evolution, especially in the software engineering world, CSS isn’t left behind; it now includes new features that allow developers to apply effects that previously would only seem possible with graphics applications such as Adobe. These new features are “CSS Filters,”which we recently covered in-depth on our blog.

Text, photos, and other aspects on a webpage can have visual effects applied to them using CSS filters. We can access effects like color, blur, and shifting on an element’s rendering before the element is displayed, thanks to the CSS filter property.

In this article, we will build a photo editor using React and some CSS filters.

 Specifically, in this article we’ll learn how to:

  • Understand the CSS filter properties with a quick recap
  • Set up our React project and start building out our application
  • Set up the JSX structure for our project.
  • Style our project using standard CSS. This includes our project sidebar.
  • Make our sidebar, containing our CSS filters, responsive using JavaScript.
  • Add some functionality to our slider using JavaScript
  • See the final app in action

A quick recap of 11 CSS filter properties

Let’s look at some of the CSS Filter properties and what they do:

  • blur(): blur() applies the blur effect to an element. If the blur value is not specified then it takes in 0 as the default value.

The result when adjusted to 70% blur:

The CoderPad logo with a blurring effect applied
The CoderPad logo is blurred by the CSS filter
  • brightness(): brightness() controls how bright the element is. It is completely black at 0% brightness, and it is identical to the original at 100% brightness. Elements are brighter when the values are higher than 100%.

The result when adjusted to 99% brightness:

The CoderPad logo with the brightness increased to the extent that the word "Pad" is no longer visible
The CoderPad logo with the brightness increased to the extent that the word “Pad” is no longer visible
  • contrast(): contrast aids in adjusting an element’s contrast. When the contrast is 0%, an element is completely black; when it is  100%, it shows the original element.

The result when adjusted to 70% contrast:

The CoderPad logo with a 70% contrast effect applied
Contrast is decreased in the CoderPad image, making all colors more contrasted
  • grayscale(): The colors of an element are changed to black and white in a grayscale. The grayscale 0% represents the original element, while 100% grayscale displays the element in grayscale entirely. Values of grayscale in between 0% and 100% are a mix of grayscale and color. Negative values are not accepted by grayscale.

The result of 100% grayscale:

The CoderPad logo with a grayscale effect applied
The CoderPad logo entirely grayscaled. The red in the logo is now a dark gray
  • hue-rotate(): The image is given a different color rotation with hue-rotate(). The value specifies how many degrees the picture samples will be rotated around the color circle. The default value of 0 degrees represents the original image.

The result when adjusted to 70% hue-rotate:

The CoderPad logo with a hue-rotate effect applied
A hue-rotated CoderPad logo, making the red look like a purple.
  • sepia(): sepia() transforms an image into a sepia image, where 100% of the image is sepia and 0% is the original image.

The result when adjusted to 70 sepia:

The CoderPad logo with a sepia effect applied
The CoderPad logo with 70% sepia applied, making the colors more brown
  • saturate(): To control an element’s saturation, saturate() is used. 100% saturate represents the original element, while 0% saturate denotes an element that is entirely unsaturated. An element that is super-saturated has a value greater than 100%.

The result when adjusted to 100 saturate:

The normal CoderPad logo, with 100% saturation, leading to a normal image

Other CSS filters include:

  • invert(): The element is reversed by invert(). The default value of 0% represents the initial image. The image is entirely inverted when the percentage is 100%. Negative values are not recognized by invert.
  • opacity(): The image’s opacity effect is controlled by opacity(). When an element has 0% opacity, it is fully transparent; when it has 100% opacity, the original element is displayed. A negative value is not recognized by this filter.
  • drop-shadow(): drop-shadow() gives the element a drop shadow appearance. It accepts the values blur, spread, color, h-shadow, and v-shadow. It can be applied to shapes as well as images because it can have the same shape as the original. The shadow will be moved to the left of the image by the negative values for the h-shadow and v-shadow.
  • url(): In order to apply some effects to an HTML element, the url filter uses a string value linking to an SVG filter element.

To learn more about CSS Filter properties with more in-depth practical examples, check out our article titled “Everything You Need to Know about All 11 CSS Filters”.

Set up a React project

The first thing we want to do in order to create our CSS-filter-powered photo editing application is to create a React project. We can do this by running the following command on our terminal:

npx create-react-app my-editor

This will automatically install all the dependencies needed to run a React app, and build a React project. To run your newly installed React app on your browser, run the npm start command. This is what it will look like in the browser:

The official starter template for React rendering. It shows the React logo spinning and “Edit src/App.js and save to reload.”

Build out the HTML using a bit of JSX

Let’s get started on adding in photo editing functionality to our newly generated React app by writing out some JSX for our application. The first thing we want to do is head into our App.js file and edit the code to look like this:

import React from 'react';
import './App.css';
import Slider from './Slider'
function App() {
  return (
    <<strong>div</strong> className='container'>
      <<strong>div</strong> className='main-image' />
      <<strong>div</strong> className='sidebar'>
       
      </<strong>div</strong>>
      <<strong>Slider</strong> />
    </<strong>div</strong>>
  );
}
export default App;Code language: PHP (php)

Here, we have created a div for our container, main-image, and sidebar. We’ll style these classes in the next section.

After this, we created a Slider.js custom component and included the following code:

import React from 'react';
export default function Slider() {
    return (
        <<strong>div</strong> className='slider-container'>
            <<strong>input</strong> type='range' ClassName='slider' />
        </<strong>div</strong>>
    )
}Code language: PHP (php)

Within the Slider component we added an input to act as our slider. We did that by creating a  div with a className of slider-container and, inside our container, added the expected input element. This is what it looks like when you run the code:

A browser pointed at localhost:3000 showing a browser’s default range slider rendered

Now we have a slider for our project that we can use later.

Style an app sidebar with CSS

Let’s do some styling in our App.css file. In our App.css file, we’ll include the following code:

*::before, *::after {
  box-sizing: border-box;
}
body {
  margin: 0;
}
.container {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-rows: 1fr auto;
  grid-template-areas:
    "image sidebar"
    "slider sidebar";
  height: 100vh;
  width: 100vw;
  background-color: #DADADA;
}
.main-image {
  grid-area: image;
  background-image: url("https://coderpad.io/wp-content/themes/coderpad/assets/logos/coderpad.svg");
  width: 100%;
  height: 100%;
  background-position: top center;
  background-size: contain;
  background-repeat: no-repeat;
}
.sidebar {
  grid-area: sidebar;
  background-color: pink;
}
.slider-container {
  grid-area: slider;
  margin-top: 2rem;
  padding: 2rem;
}
.slider {
  width: 100%;
  cursor: pointer;
}Code language: CSS (css)

In this code snippet, we first set our margin  to 0, to remove any margin from our body. Then we styled the rest of our project to make sure our image takes as much space as possible while everything else is pushed to the side.

The image below shows the result of the CSS above:

A pink sidebar rendered to the right-hand side of the page. In the center of the page is the CoderPad logo and on the bottom of the page is our input slider.

Sidebar

Before we style our sidebar, we need to actually put some content inside of it. To do that, we’ll first create a SidebarItem component in our App.js file:

function App() {
  return (
    <<strong>div</strong> className='container'>
      <<strong>div</strong> className='main-image' />
      <<strong>div</strong> className='sidebar'>
        <<strong>SidebarItem</strong> />
        <<strong>SidebarItem</strong> />
      </<strong>div</strong>>
      <<strong>Slider</strong> />
    </<strong>div</strong>>
  );
}
export default App;Code language: PHP (php)

Then we’ll create a SidebarItem.js component in our src folder, where we can include the following code:

import React from 'react'
export default function SidebarItem() {
    return(
        <<strong>button</strong> className='sidebar-item'>Sidebar Item</<strong>button</strong>>
    )
}Code language: JavaScript (javascript)

This is the result in the image below:

Two browser default styled buttons inside of the pink sidebar off to the right of the screen.

Sidebar CSS

OK! Now to style our sidebar. Let’s head into our App.css file again to make the following changes:

.sidebar {
  grid-area: sidebar;
  background-color: pink;
  border-left: 1px solid hsl(265, 100%, 56%);
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.sidebar-item {
  cursor: pointer;
  border: none;
  outline: none;
  background-color: pink;
  padding: 1rem;
  position: relative;
  transition: background-color 150ms;
}
.sidebar-item:hover, .sidebar-item:focus {
  background-color: hsl(265, 100%, 76%);
}
.sidebar-item::after {
  content: ' ';
  position: absolute;
  width: 80%;
  left: 10%;
  bottom: 0;
  height: 1px;
  background-color: hsl(265, 100%, 46%);
}
.sidebar-item:last-child::after {
  display: none;
}Code language: CSS (css)

Using the above CSS, we styled our sidebar to give us the result in the GIF below:

A right-handed sidebar with a darker background hover state and a light fade effect between hovering and not.

Add in sidebar functionality with React

Now that we’ve gotten styling out of the way – let’s get going on the fun part: where we add in different controls for CSS Filter options. To get started, we’ll first write some code in our App.js file:

import React, {useState, useMemo} from 'react';
import './App.css';
import Slider from './Slider'
import SidebarItem from './SidebarItem'

const getDefaultOptions = () => [
  {
    name: 'Brightness',
    property: 'brightness',
    value: 100,
    range: {
      min: 0,
      max: 200
    },
    unit: '%'
  },
  {
    name: 'Contrast',
    property: 'contrast',
    value: 100,
    range: {
      min: 0,
      max: 200
    },
    unit: '%'
  },
  {
    name: 'Saturation',
    property: 'saturate',
    value: 100,
    range: {
      min: 0,
      max: 200
    },
    unit: '%'
  },
  {
    name: 'Grayscale',
    property: 'grayscale',
    value: 0,
    range: {
      min: 0,
      max: 100
    },
    unit: '%'
  },
  {
    name: 'Sepia',
    property: 'sepia',
    value: 0,
    range: {
      min: 0,
      max: 100
    },
    unit: '%'
  },
  {
    name: 'Hue Rotate',
    property: 'hue-rotate',
    value: 0,
    range: {
      min: 0,
      max: 360
    },
    unit: 'deg'
  },
  {
    name: 'Blur',
    property: 'blur',
    value: 0,
    range: {
      min: 0,
      max: 20
    },
    unit: 'px'
  }
];
function App() {
  const [selectedOptionIndex, setSelectedOptionIndex] = useState(0)
  const [options, setOptions] = useState(getDefaultOptions())
  const selectedOption = options[selectedOptionIndex]
  return (
    <div className='container'>
      <div className='main-image' />
      <div className='sidebar'>
        {options.map((option, index) => {
          return (<SidebarItem
            key={index}
            name={option.name}
            active={index === selectedOptionIndex}
            handleClick={() => setSelectedOptionIndex(index)}
          />
          )
        })}
      </div>
      <Slider />
    </div>
  );
}
export default App;Code language: JavaScript (javascript)

First, we create a variable DEFAULT_OPTIONS , then we set it to an array of objects. These objects define each one of our buttons, with each of them containing the names of the CSS filter we are using for the project. For each of our objects, there is a property field that is set to a CSS filter name, and this is what it corresponds to in CSS.

We also have our default value, the range which is essentially the min and max of our slider, then we have our unit for each CSS filter object.

Next, we’ll take our const DEFAULT_OPTIONS, and turn them into an options object we can use in React, and modify it. We’ll also edit our sidebar.js file to look like this:

import React from 'react'
export default function SidebarItem({ name, active, handleClick  }) {
    return(
        <<strong>button</strong>
        className={`sidebar-item ${active ? 'active' : ''}`}
        onClick={handleClick}
        >
            { name }
            </<strong>button</strong>>
    )
}Code language: PHP (php)

Here, we’re taking the name from the props in our App.js file like this; { name }. Below are the results of our code above:

A list of CSS filters in the right-hand sidebar that includes the following filters: Brightness, Contrast, Saturation, Grayscale, Sepia, Hue Rotate, and Blur

Make the CSS filter input slider functional

Now we’ll look at how to make our slider correspond with our active value. In our App.js file, we’ll include the following code:

<Slider
        min={selectedOption.range.min}
        max={selectedOption.range.max}
        value={selectedOption.value}
        handleChange={handleSliderChange}
      />Code language: HTML, XML (xml)

In our Slider, we passed in different props than we had previously, which in our case is the min, max, value, and handleChange. Then, inside of App, we created a function to handle changes in selected options for us:

function App() {
  const [selectedOptionIndex, setSelectedOptionIndex] = useState(0)
  const [options, setOptions] = useState(DEFAULT_OPTIONS)
  const selectedOption = options[selectedOptionIndex]


  function handleSliderChange({ target }) {
    setOptions(prevOptions => {
      return prevOptions.map((option, index) => {
        if (index !== selectedOptionIndex) return option
        return { ...option, value: target.value }
      })
    })
  }Code language: JavaScript (javascript)

Now that we have our function created, we are passing all the needed values down to our Slider. Now, in our slider.js file let’s use these new values. To do this, we’ll add the following code:

import React from 'react'
export default function Slider({ min, max, value, handleChange}) {
  return (
    <<strong>div</strong> className="slider-container">
      <<strong>input</strong>
        type="range"
        className="slider"
        min={min}
        max={max}
        value={value}
        onChange={handleChange}
      />
    </<strong>div</strong>>
  )
}Code language: PHP (php)

We now have the results below:

A user clicking between different CSS filter options and changing the input slider associated with this CSS filter. These options do not yet impact the image in any way

As shown in the example above, when we change these different values, it updates the values of the filter selected internally.

While this is great progress, it’s not yet applying those CSS filters to the image itself. Let’s fix that.

The final product: Apply CSS filters to an image

All that is left to do to make our image editor functional is take all the values and add them to our image and do the filtering for us:

<strong>const</strong> getImageStyle = useMemo (<strong>function</strong> <strong>getImageStyle</strong>() {
 
<strong>const</strong> filters = options.map(option => {
    <strong>return</strong> `${option.property}(${option.value}${option.unit})`
  }
)

  <strong>return</strong> { filter: filters.join(' ') }
}, [options])


console.log(getImageStyle)

  <strong>return</strong> (
    <div className='container'>
      <div className="main-image" style={getImageStyle} />
      <div className='sidebar'>
        {options.map((option, index) => {
          <strong>return</strong> (<SidebarItem
            key={index}
            name={option.name}
            active={index === selectedOptionIndex}
   
   
  handleClick={() => setSelectedOptionIndex(index)}
        />
        )
      })}Code language: HTML, XML (xml)

In our App.js file, we first create a const getImageStyle = useMemo, which is going to contain the style we apply to our image. In our function, we created our const filters = options. map. This map loops through all of our options to get them converted to a usable CSS property. 

We are now done with our demo photo editor app with React and CSS Filters.

Take a look at the practical results.

Here’s us modifying Brightness, Contrast, and Saturation:

A user changing the brightness of an image all the way down and back up, then doing the same with contrast and saturation. The CSS filters work the same way a traditional photo editing app would.

Now here’s an example of us modifying the Grayscale, Sepia, Hue Rotate, and Blur:

The user is modifying different color properties, such as grayscale, sepia, and hue rotate. Then, they’re adding a blur to the final result.

Try out the photo editor in the sandbox below:

Wrapping up

Wow! We’ve learned quite a bit just by creating a simple photo editing application that uses CSS filters to apply different effects to an image. 

If you want to see the full source code for the React application we built today, you can see the source code over on my GitHub repository containing the project.

Once you’ve mastered CSS filters, you’ll likely want to use them in combination with CSS math functions throughout your app’s styling. Here’s an article outlining what CSS math functions are, and how to use them.

Lastly, as you continue to expand your React photo editor, you’ll need a way to show your users different pages based on the URL. Here’s how you can add in a routing solution into React.

Hopefully this article has been helpful in learning how to practically apply CSS filters and build an app of your own. Until next time!

Uma Victor is a software developer based in Nigeria who is familiar with a variety of different web technologies and frameworks. He is also keen on finding ways to explain things as simple as possible. You can reach out to me on twitter.