Smooth Shadows for Images Using Their Dominant Color
If you’ve seen posts about Neumorphism (opens in a new tab) or CSS generators like neumorphism.io (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) site. The end result will also be the same as this Codesandbox (opens in a new tab) you can look at and fork.

#Prerequisites
While not necessary for this technique to work I’m using Gatsby (opens in a new tab) and gatsby-plugin-image
(opens in a new tab) 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 (opens in a new tab) to process your images and get back information like the dominant color in any JS framework. For React there’s also color-thief-react (opens in a new tab) (although I haven’t tried that personally). The library polished (opens in a new tab) 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) } } }`
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
polished (opens in a new tab) is “a lightweight toolset for writing styles in JavaScript” and features handy helper functions, including rgba
(opens in a new tab) 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 importsimport { 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
But how does one get the correct values for the three arrays? @brumm (opens in a new tab) created the awesome website Smooth Shadow (opens in a new tab) 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);
But that’s a black shadow 😬 Time to make a colourful one. You can translate the generated values into their respective arrays.
As always, MDN has a good explanation on box-shadow (opens in a new tab). 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);
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 importsimport { 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
#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) } } }`
The last step is to use the style
prop (opens in a new tab) 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), }} /> ))}
#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 (opens in a new tab) and replace the rgb with thecolor
param - @brumm (opens in a new tab) has pointed out to me on Twitter (opens in a new tab) that you can also use eaze (opens in a new tab) to programmatically create the shadow easing values
- Use the developer tools inside your browser, go to the “Sources” tab and browse the source code of Smooth Shadow (opens in a new tab) to reverse engineer the functions so that
generateShadow
can take the same params as the webpage
#Shadow Palette Generator
Josh W. Comeau introduced his “Shadow Palette Generator” (opens in a new tab) (you can play with it here (opens in a new tab)) and you can use it for image shadows, too, of course!
His generator outputs CSS Custom Properties which you can use in your boxShadow
. Instead of creating a generateShadow
function you’ll only need to convert the dominant color to HSL.
Your page might look something like this then:
import React from "react"import { graphql } from "gatsby"import { GatsbyImage, getImage } from "gatsby-plugin-image"import { parseToHsl } from "polished"
const ELEVATIONS = { high: `0px 0.8px 0.8px hsl(var(--shadow-color) / 0.94), 0.1px 14.4px 15.1px -1.5px hsl(var(--shadow-color) / 0.84), 0.3px 68.7px 72.1px -3px hsl(var(--shadow-color) / 0.73)`,}
export default function Home({ data }) { return ( <main> <h1>Images with Dominant Color Smooth Shadows</h1> <div> {data.images.nodes.map((image) => { const color = parseToHsl(image.gatsbyImageData.backgroundColor)
return ( <GatsbyImage alt="" image={getImage(image)} style={{ "--shadow-color": `${color.hue} ${color.saturation} ${color.lightness}`, "--shadow-elevation-high": ELEVATIONS.high, boxShadow: `var(--shadow-elevation-high)`, }} /> ) })} </div> </main> )}
export const query = graphql` { images: allImageSharp { nodes { gatsbyImageData(quality: 90, width: 800, placeholder: DOMINANT_COLOR) } } }`