DEV Community

Cover image for How To Make Adaptive Card Color Depending On Image Background
mcanam
mcanam

Posted on

How To Make Adaptive Card Color Depending On Image Background

Hi, this time we will experiment with creating a simple UI component that can change color according to the background image.

I was inspired by the music player notification on my phone:

reference

And the experiment result:

result

looks cool right? now let's start making it πŸš€

Preparation

we need to create html, css, and js files first

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Adaptive Card Color</title>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <script src="script.js"></script>
</body>

</html>

Enter fullscreen mode Exit fullscreen mode

style.css

*,
*::before,
*::after {
    padding: 0; margin: 0;
    box-sizing: border-box;
}

html {
    font-family: sans-serif;
    font-size: 16px;
    line-height: 1.5;
}

body {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
Enter fullscreen mode Exit fullscreen mode

script.js

console.log("hello world");
Enter fullscreen mode Exit fullscreen mode

Make card component

create html structure of the card like this

<div class="card">
    <img class="card-image" src="">
    <div class="card-text">
        <span>Lorem Ipsum</span>
        <span>Is simply dummy text of the printing and typesetting industry.</span>
    </div>
 </div>
Enter fullscreen mode Exit fullscreen mode

don't forget the style

.card {
    position: relative;
    overflow: hidden;
    width: 90%;
    max-width: 400px;
    margin-bottom: 30px;
    padding: 30px;
}

.card-image {
    position: absolute;
    top: 0; right: 0;
    width: auto;
    height: 100%;
    object-fit: cover;
    object-position: center;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000);
            mask-image: linear-gradient(90deg, transparent, #000);
}

.card-text {
    position: relative;
    z-index: 2;
    max-width: 75%;
    display: flex;
    flex-direction: column;
}

.card-text span:first-child {
    font-weight: 500;
    font-size: 1.2rem;
    margin-bottom: 5px;
}

.card-text span:last-child {
    opacity: 0.7;
}
Enter fullscreen mode Exit fullscreen mode

For image source you can get it from unsplash or use a local file.

Extract color from image

Yes, we have to extract color from image and it's a quite complex job. Luckily, i found a cool library that will do for us https://github.com/lokesh/color-thief

add the library to our project

<script src="https://unpkg.com/colorthief@2.3.2/dist/color-thief.umd.js"></script>
Enter fullscreen mode Exit fullscreen mode

Javascript Time

// get all card elements.
const cards = document.querySelectorAll(".card");

// create colorthief instance
const colorThief = new ColorThief();

cards.forEach(async (card) => {
    const image = card.children[0];
    const text = card.children[1];

    // get palette color from image
    const palette = await extractColor(image);

    const primary = palette[0].join(",");
    const secondary = palette[1].join(",");

    // change color
    card.style.background = `rgb(${primary})`;
    text.style.color = `rgb(${secondary})`;
});

// async function wrapper
function extractColor(image) {
    return new Promise((resolve) => {
        const getPalette = () => {
            return colorThief.getPalette(image, 4);
        };

        // as said in the colorthief documentation, 
        // we have to wait until the image is fully loaded.

        if (image.complete) {
            return resolve(getPalette());
        }

        image.onload = () => {
            resolve(getPalette());
        };
    });
}
Enter fullscreen mode Exit fullscreen mode

If you get an error and you use image from internet, you can add crossorigin="anonymous" attribute on the img tag.

Next task, check if the primary color is dark or light so we can correctly choose color for the text. Happy experimenting :D

Thank you very much for reading. Don't hesitate to leave comments, criticisms or suggestions, I will really appreciate it ☺️

Image cover by Hasmik Ghazaryan Olson on Unsplash

Discussion (3)

Collapse
lukeshiru profile image
Luke Shiru

You could simplify it quite a bit and make more use of CSS. I would extract the logic for image loading into a small utility function for running a callback when images load (or if they are already loaded):

const imageLoadHandler = handler => image =>
    image !== null
        ? image.complete
            ? handler(image)
            : image.addEventListener("load", () => handler(image))
        : undefined;
Enter fullscreen mode Exit fullscreen mode

And then I would use that from the forEach for the cards, but instead of setting styles directly from JS into the elements, I would just have 2 CSS custom properties:

document.querySelectorAll(".card").forEach(card =>
    imageLoadHandler(image => {
        const [primary, secondary] = colorThief.getPalette(image, 4);

        card.style.setProperty("--primary", `rgb(${primary.join(",")})`);
        card.style.setProperty("--primary", `rgb(${secondary.join(",")})`);
    })(card.querySelector(".card-image")),
);
Enter fullscreen mode Exit fullscreen mode

This way, from CSS you can use those colors wherever you want inside the card, so to replicate what you have we only need to:

.card {
  background-color: var(--primary, #000);
}

.card-text {
  color: var(--secondary, #fff);
}
Enter fullscreen mode Exit fullscreen mode

You might notice that with this approach, you can reuse --primary and --secondary wherever you want inside the card.

Even if you prefer not to go with this approach, I would still strongly recommend to avoid code like this:

const image = card.children[0];
const text = card.children[1];
Enter fullscreen mode Exit fullscreen mode

Because that depends on the order in the DOM, which isn't ideal, so instead is better to do something like this:

const image = card.querySelector(".card-image");
const text = card.querySelector(".card-text");
Enter fullscreen mode Exit fullscreen mode

With that you can safely move the elements inside .card around without messing your JS.

Cheers!

Collapse
shshank profile image
Shshank

This post and comment is really helpful, thank you for sharing this info πŸ˜„

Collapse
mcanam profile image
mcanam Author

Thank you very much for the corrections and suggestions πŸ™Œ. I'll update the article soon.

I learned something new today πŸ˜†