DEV Community

Lennart
Lennart

Posted on • Originally published at lekoarts.de

Smooth Shadows for Images using their Dominant Color

If you've seen posts about Neumorphism or CSS generators like neumorphism.io you're probably familiar with these super smooth shadows the elements have. While designing a whole page in this style would be a bit too much for me personally I do like the shadows! In fact, at some point the design blog Abduzeedo had smooth shadows on their images (using the dominant color) -- so exactly what I'll show today.

You can see a preview of the effect on my Emilia Theme site. The end result will also be the same as this Codesandbox you can look at and fork.

Preview of the finished result. Heading saying "Images with Dominant Color Smooth Shadows" and below are four images (colorful wall, a bee in orange flowers, green lego bricks, and a house with lot of blue sky behind it) that each have a colorful smooth shadow. The dominant color is used for the color of the shadow.

Prerequisites

While not necessary for this technique to work I'm using Gatsby and gatsby-plugin-image to handle and display the images. I'm doing this because gatsby-plugin-image and its gatsbyImageData supports the placeholder value DOMINANT_COLOR and gives back this value as backgroundColor -- so you can directly query the dominant color of an image.

Set up a new site and install the necessary plugins for gatsby-plugin-image following its instructions, e.g. with npm init gatsby and the Add responsive images option at the end.

You can use Color Thief to process your images and get back information like the dominant color in any JS framework. For React there's also color-thief-react (although I haven't tried that personally). The library polished will also work in any JS framework.

Query your images and make sure that you have the DOMINANT_COLOR option for the placeholder for gatsbyImageData. An example page could be:

import React from "react"
import { graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"

export default function Home({ data }) {
  return (
    <main>
      <h1>Images with Dominant Color Smooth Shadows</h1>
      <div>
        {data.images.nodes.map((image) => (
          <GatsbyImage alt="" image={getImage(image)} />
        ))}
      </div>
    </main>
  )
}

export const query = graphql`
  {
    images: allImageSharp {
      nodes {
        gatsbyImageData(quality: 90, width: 800, placeholder: DOMINANT_COLOR)
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

image.gatsbyImageData.backgroundColor inside the .map() will give back the dominant color.

Creating the function to generate shadows

Create a new function called generateShadow with the single argument color in your page. As the function will use a method from another library you'll first need install polished.

npm install polished
Enter fullscreen mode Exit fullscreen mode

polished is "a lightweight toolset for writing styles in JavaScript" and features handy helper functions, including rgba which you'll use to create a RGBA color string inside the generateShadow function.

The generateShadow function will take a color and iterate over the arrays shadowX, shadowY, and transparency internally to create an array of valid box-shadow strings. It returns a string that you can use with box-shadow in CSS since you can chain them with a comma.

// Rest of imports
import { rgba } from "polished"

function generateShadow(color) {
  const shadowX = []
  const shadowY = []
  const transparency = []

  let shadowMap = []

  for (let i = 0; i < 6; i++) {
    const c = rgba(color, transparency[i])

    shadowMap.push(`0 ${shadowX[i]} ${shadowY[i]} ${c}`)
  }

  return shadowMap.join(", ")
}

// Rest of page
Enter fullscreen mode Exit fullscreen mode

But how does one get the correct values for the three arrays? @brumm created the awesome website Smooth Shadow which you can use to get these values. For my purposes I used 6 layers and only changed the final transparency to 0.15.

So you'll get the CSS:

box-shadow: 0 2.8px 2.2px rgba(0, 0, 0, 0.042), 0 6.7px 5.3px rgba(0, 0, 0, 0.061),
  0 12.5px 10px rgba(0, 0, 0, 0.075), 0 22.3px 17.9px rgba(0, 0, 0, 0.089),
  0 41.8px 33.4px rgba(0, 0, 0, 0.108), 0 100px 80px rgba(0, 0, 0, 0.15);
Enter fullscreen mode Exit fullscreen mode

But that's a black shadow 😬 Time to make a colorful one. You can translate the generated values into their respective arrays.

As always, MDN as a good explanation on box-shadow. The generated CSS from "Smooth Shadow" is in this syntax:

/* offset-x | offset-y | blur-radius | spread-radius | color */
box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.2);
Enter fullscreen mode Exit fullscreen mode

So offset-x goes into shadowX, offset-y into shadowY, and the last value of rgba into transparency.

Depending on your values your generateShadow function now should look something like this:

// Rest of imports
import { rgba } from "polished"

function generateShadow(color) {
  const shadowX = ["2.8px", "6.7px", "12.5px", "22.3px", "41.8px", "100px"]
  const shadowY = ["2.2px", "5.3px", "10px", "17.9px", "33.4px", "80px"]
  const transparency = [0.042, 0.061, 0.075, 0.089, 0.108, 0.15]

  let shadowMap = []

  for (let i = 0; i < 6; i++) {
    const c = rgba(color, transparency[i])

    shadowMap.push(`0 ${shadowX[i]} ${shadowY[i]} ${c}`)
  }

  return shadowMap.join(", ")
}

// Rest of page
Enter fullscreen mode Exit fullscreen mode

Apply shadows to images

Now it's time to use generateShadow. Your complete page now should look something like this:

import React from "react"
import { graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import { rgba } from "polished"

function generateShadow(color) {
  const shadowX = ["2.8px", "6.7px", "12.5px", "22.3px", "41.8px", "100px"]
  const shadowY = ["2.2px", "5.3px", "10px", "17.9px", "33.4px", "80px"]
  const transparency = [0.042, 0.061, 0.075, 0.089, 0.108, 0.15]

  let shadowMap = []

  for (let i = 0; i < 6; i++) {
    const c = rgba(color, transparency[i])

    shadowMap.push(`0 ${shadowX[i]} ${shadowY[i]} ${c}`)
  }

  return shadowMap.join(", ")
}

export default function Home({ data }) {
  return (
    <main>
      <h1>Images with Dominant Color Smooth Shadows</h1>
      <div>
        {data.images.nodes.map((image) => (
          <GatsbyImage alt="" image={getImage(image)} />
        ))}
      </div>
    </main>
  )
}

export const query = graphql`
  {
    images: allImageSharp {
      nodes {
        gatsbyImageData(quality: 90, width: 800, placeholder: DOMINANT_COLOR)
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

The last step is to use the style prop from gatsby-plugin-image to apply the box-shadow to the outer wrapper of <GatsbyImage />.

{
  data.images.nodes.map((image) => (
    <GatsbyImage
      alt=""
      image={getImage(image)}
      style={{
        boxShadow: generateShadow(image.gatsbyImageData.backgroundColor),
      }}
    />
  ))
}
Enter fullscreen mode Exit fullscreen mode

Bonus 🍬

If you want to practise some skills you have and/or go beyond this little guide, here are some ideas:

  • Change generateShadow to take in the generated shadow from Smooth Shadow and replace the rgb with the color param
  • Use the developer tools inside your browser, go to the "Sources" tab and browse the source code of Smooth Shadow to reverse engineer the functions so that generateShadow can take the same params as the webpage

Top comments (0)