Zach’s ugly mug (his face) Zach Leatherman

Extract Colors from an Image for CSS Themes

February 27, 2025

Working through the migration of Blog Awesome from WordPress to Eleventy, I encountered an interesting problem: each blog post was seeded with a featured image at the top, though the image did not cover the entirety of the header.

Screenshot of blog.fontawesome.com with a featured image that says Font Awesome + 11ty

This required a manual process (a WordPress custom field) to specify a theme color to match the background of the image. We can do better!

Using a similar layout structure, this what the new design looks like (with a bit better automatic dark/light mode contrast on the flag and text color, too):

Screenshot of the new blog.fontawesome.com with a featured image that says Font Awesome + 11ty

To accomplish this automation, I turned to a lovely zero-dependency package called extract-colors from namide.com.

In all of the eligible blog posts tested, the last color returned in the result from the extract-colors package always matched the background color of the image being sampled.

extract-colors recommends the use of another package (get-pixels) to extract pixel data from images but it was no longer maintained so I forked, updated, and released a Node.js only version of the package to fix some upstream issues.

New @11ty/image-color Package

I wired this up with a memoization layer, a disk cache, a concurrency queue, and integrated it with existing overlapping functionality provided by Eleventy Fetch and Image utilities for build performance (as well as adding Color.js for some color filtering) and packaged this all up for anyone to use at @11ty/image-color:

Screenshot image for https://v1.screenshot.11ty.dev/https%3A%2F%2Fgithub.com%2F11ty%2Fimage-color/opengraph//

To get colors from a local or remote Image in my Eleventy project, I added the following configuration code to my project’s eleventy.config.js file:

import { getImageColors } from "@11ty/image-color";

export default async function(eleventyConfig) {
	eleventyConfig.addFilter("getImageColors", async (imageSrc) => {
		return getImageColors(imageSrc);
	});
}

The above Blog Awesome example above made good use of the getImageColors filter in a Nunjucks template:

{%- set lastColor = media.featuredImage | getImageColors | last %}
{% if lastColor %}
	<style>
	header {
		background-color: {{ lastColor.background }};
		color: {{ lastColor.foreground }};
	}
	</style>
{% endif %}
<header></header>

Screenshot Borders

For extra funsies I also made use of this functionality on 11ty.dev (now live on the site) to provide an extra accent border color on screenshot images:

A 4×4 matrix of small screenshots of the Built With Eleventy section on the 11ty.dev home page. Each screenshot has a border color that matches the favicon image

This example works a little differently: it samples colors from the favicon images of each site as an easy way to guess the site’s theme colors.

This package doesn’t take a hard stance on the validity of colors but I did make use of additional filtering to select a nice border color from the list of colors available in each favicon, with the code looking something like this (again, eleventy.config.js Configuration code):

import { getImageColors } from "@11ty/image-color";

export default async function(eleventyConfig) {
	eleventyConfig.addShortcode("getColorsForUrl", async (url) => {
		let avatarUrl = `https://v1.indieweb-avatar.11ty.dev/${encodeURIComponent(url)}/`;

		return getImageColors(avatarUrl).then(colors => {
			// Note the map to colorjs props here
			return colors.map(c => c.colorjs).filter(c => {
				// Not too dark, not too light
				return c.oklch.l > .4 && c.oklch.l <= .95;
			}).sort((a, b) => {
				return (b.oklch.l + b.oklch.c) - (a.oklch.l + a.oklch.c);
			});
		})
	});
};

…which subsequently wound up in a WebC template a little like this:

<script webc:setup>
async function getPrimaryColorStyle(url) {
	let colors = await getColorsForUrl(url);
	if(colors.length > 0) {
		return `--card-border-color: ${colors[0].toString({format: "hex"})}`;
	}
}
</script>
<a :href="url" class="card" :style="getPrimaryColorStyle(url)"></a>

More Open Source

This Blog Awesome migration project (launching soon!) has yielded a few more useful open source utilities to the Eleventy ecosystem, which I encourage you to try out!


Older >
?nodefine — a pattern to skip Custom Element definitions

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is a builder for the web at Font Awesome and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 84 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

12 Reposts

IndieWeb Avatar for https://www.alvinashcraft.comRyan MulliganAgénor DebriatJamie YorkAndy BellDavid Sleight✿ Floby ????????????Eric PortisKarin!Bob MonsourBaldur BjarnasonJean Pierre Kolb

64 Likes

E. Erdem205TFgeeky Chakri ????fmColin Johnstonhenry ✷Tony MessiasMyles LewandokrismcdchristianjxhxnnclaasAgénor DebriatIndieWeb Avatar for https://bsky.appDavid M. Schulman ????Dominik SchreiberTyler CombsRoderick GadellaaAlistairMatt Hobbs (he/him)Tiago CoutoAndy BelllionbyteEvil Jim O’DonnellDanielAlberto CalvoMatt Gaunt-SeoNate SpilmanJack OsborneDavid SleightBenoit C. ????Richard RutterTroels ThomsenHannu Aarniala (He/Him/Hän)treb0rMiguel CalderonFynn Ellie BeckerLiam ????️‍????Eric PortisNaumAnge ChierchiaLynn FisherAlex GuyotNannnsssAmberBaldur BjarnasonEric McCormickkiran :unreal: :krita:damianwalshCarlTrist ????MichaelJon Robertskeith kursonElio StruyfIlja | ИльяHeywisemanjack_the_devChristian AlderRené
6 Comments
  1. Eric Portis

    @zachleat 1) Yellows FTW (C+L champions) 2) What do you use when there is no color that meets your 0.4<x<0.95 filter? 3) Did something go wrong without the concurrency queue that led you to it, or was it a defensive move? I don't have good intuitions about when adding s… Truncated

  2. Christian Alder

    Yay! I was hoping there might be a blog post when I saw that plugin drop the other day ???? Thanks!

  3. Zach Leatherman :11ty:

    @eeeps I think the default is just a light gray but it definitely happens! Yeah, I usually try to add a concurrency queue (and heavier caching) for things that can/might fetch out to remote sources — definitely a defensive move after I saw some build failure flakiness in some de… Truncated

  4. Eric Portis

    @zachleat Innnnteresting (about the concurrency queue) thank you!

  5. Shack

    ????

  6. claas

    Neat, I can use this to extract colors to use with my Tailwind CSS plugin www.npmjs.com/package/@cla...

Shamelessly plug your related post

These are webmentions via the IndieWeb and webmention.io.

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)