Skip to content

Smooth Shadows for Images Using Their Dominant Color


Created: Jul 28, 2021 – Last Updated: Nov 22, 2021

Tags: Gatsby, React

Digital Garden

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.

Preview of the finished result. Heading saying "Images with Dominant Color Smooth Shadows" and below are four images (colourful wall, a bee in orange flowers, green lego bricks, and a house with lot of blue sky behind it) that each have a colourful 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 (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.

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

src/pages/index.js
jsx
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.

sh
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.

src/pages/index.js
jsx
// 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

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:

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.

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

src/pages/index.js
jsx
// 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

#Apply shadows to images

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

src/pages/index.js
jsx
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 />.

jsx
{
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:

#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:

src/pages/index.js
jsx
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)
}
}
}
`

Want to learn more? Browse my Digital Garden