Recreating Android’s ‘Monet’ colour schemes in your web app

7 min intermediate
Mike Booth 8 November 2021
Follow

What is Monet?

If you’re already a developer and just want to get straight to the goods, you can skip ahead with these links:

Monet

For everyone else, Google recently rolled out Android 12, including its new, wallpaper-based theme engine called Monet. Monet is one of the main features of Google’s Material You project, which is designed to create more personalised experiences and unified-feeling interfaces. Specifically, Monet creates custom colour palettes for the Android user-interface based upon on the dominant colour of the user’s wallpaper. And through some magic, it’ll ensure that regardless of what colour your wallpaper is, text and other elements will always be legible against their backgrounds, and even pass contrast accessibility standards for users with reduced eyesight.

Change your wallpaper on your Pixel and your entire Android 12 experience changes to match. Using advanced color extraction algorithms you can easily personalize the look and feel of your entire phone, including notifications, settings, widgets and even select apps.

This created quite a stir amongst the UI-design community, as being revolutionary in terms of personalisation and consistency.

Building using jQuery

Like all innovation, it was not a completely new idea; rather a combination of existing ideas. I set out to combine these ideas as hastily as possible to recreate an equivalent system which could be applied to web-apps, not just Android applications.

Despite being a little old-school, I chose jQuery, because:

  • jQuery has been around a long time, so it has millions of plugins, including ones with the functionality this project required.
  • I’m familar with it, so I could focus on this prject instead of learning new libraries.
  • I could set it up within seconds.

There are two jQuery plugins needed for this project:

  1. Color Thief – Extracts the dominant colour and/or colour palette from any image which you supply.
  2. jQuery xColor – Creates colour palettes from a base colour using various colour theory.

Steps

If you want to see the whole thing, just skip to the bottom of this guide.

1. Set up the HTML

This is just a really basic structure with an image and a few mocked-up UI elements made to look a little like an Android homescreen.

There’s also a ‘drop menu’ that lets you try different images.

<!doctype html>
<html class="no-js" lang="">
    <head>
        <meta name="robots" content="noindex">
        <meta charset="utf-8">
        <meta http-equiv="x-ua-compatible" content="ie=edge">
        <title>Dominant Color</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            * {
                font-family: sans-serif;
            }
        </style>
    </head>
    <body>
        <div style="margin: 2em auto; width: 300px;">
            <select id="image-selector" style="width: 100%; font-size: 1.25em; padding: 0.25em; border-radius: 0.25em;">
                <option value="img/image1.jpg">Image 1</option>
                <option value="img/image2.jpg">Image 2</option>
                <option value="img/image3.jpg">Image 3</option>
                <option value="img/image4.jpg">Image 4</option>
                <option value="img/image5.jpg">Image 5</option>
                <option value="img/image6.jpg">Image 6</option>
                <option value="img/image7.jpg">Image 7</option>
                <option value="img/image8.jpg">Image 8</option>
            </select>
        </div>
        <div id="container" style="width: 300px; height: 600px; margin: auto; position: relative; bordeR: 2px solid white;">
            <img id="main-image" src="img/image1.jpg" style="width: 100%;">
            <div class="primary-background-color" style="width: 100%; height: 1em; position: absolute; top: 0; left: 0; padding-top:0.25em;">
                <span style="font-size: 0.75em; color: white; display: block; float: left; padding-left: 0.5em;" ;>
                    14:27
                </span>
                <span style="float: right;">
                    <i class="fa fa-volume-down" style="font-size: 0.75em; display: inline-block; margin-right: 0.5em; color: white; float: right;"></i>
                    <i class="fa fa-wifi" style="font-size: 0.75em; display: inline-block; margin-right: 0.5em; color: white; float: right;"></i>
                    <i class="fa fa-battery-three-quarters" style="font-size: 0.75em; display: inline-block; margin-right: 0.5em; color: white; float: right;"></i>
                </span>
            </div>
            <div style="position: absolute; border-radius: 20px; background-color: white; padding: 10px; top: 30%; right: 20px;">
                <div class="primary-background-color" style="width: 15px; height: 100px; border-radius: 10px; margin: auto;">
                </div>
                <i class="fa fa-volume-down primary-color" style="font-size: 0.75em; display: block; text-align: center; padding-top: 0.5em;"></i>
            </div>
            <span class="complimentary-color" style="position: absolute; top: 40%; left: 20px; font-size: 1.5rem; max-width: 200px; transform: translateY(-50%); font-family: 'Raleway', sans-serif; font-weight: 600;">
                Wallpaper-based colour themes
            </span>
        </div>
        <script src="https://kit.fontawesome.com/47f5690e4a.js" crossorigin="anonymous"></script>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Raleway:wght@600&display=swap" rel="stylesheet">
    </body>
</html>

2. Include your Javascript libraries

I included these right in the project before the footer.

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="js/color-thief/dist/color-thief.min.js"></script>
<script src="js/jquery.xcolor.js"></script>

3. Build your function

We’re going to put everything within a function called colorGen(). We start by intializing the plugins, but we’ll keep building this function up over the next few steps.

function colorGen() {
    // Create color thief
    var colorThief = new ColorThief();
    // Create image object from the image of choice.
    var sourceImage = $('#main-image')[0];
}

4. Get the dominant colour from the image

Next, add to our function the image that we’re using.
We then use the getColor() method of Colorthief to determine the dominant colour from our image.

function colorGen() {
    // Create color thief
    var colorThief = new ColorThief();
    // Create image object from the image of choice.
    var sourceImage = $('#main-image')[0];

    /*=================================================================*/
    /*------------------ Set the primary colour -----------------------*/
    /*=================================================================*/
    // Get the dominant color of image
    var dominantColor = colorThief.getColor(sourceImage);
}

5. Make a lighter tint of the primary colour

Next we need to imitate the ‘pastel’ colour style used by Monet.  We’re going to do use this method to get the lightest possible tint of the dominant colour that’ll still be readable against a white background for our titles and icons.

This is done by taking the dominant colour, and lightening it over and over again in a whileloop until it no longer passes ‘readable’ against white (#fff). This is where the xcolor’s lighten() and readable() methods come into play. Once we’ve worked out how light we can go, we create CSS classes for .primary-background-color and .primary-color from this value.

function colorGen() {
    // Create color thief
    var colorThief = new ColorThief();
    // Create image object from the image of choice.
    var sourceImage = $('#main-image')[0];

    /*=================================================================*/
    /*------------------ Set the primary colour -----------------------*/
    /*=================================================================*/
    // Get the dominant color of image
    var dominantColor = colorThief.getColor(sourceImage);
    // Loop to lighten the primary colour until it is as light as it can be whilst remaining readable on white. This gives it monet's 'pastel' look.
    var themeColor = dominantColor;
    if($.xcolor.readable(themeColor, '#fff') == true) {
        while ($.xcolor.readable(themeColor, '#fff') == true) {
            themeColor = $.xcolor.lighten(themeColor, step = 1, shade = 32);
            $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
            $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
        }
    } else {
        while ($.xcolor.readable(themeColor, '#fff') == false) {
            themeColor = $.xcolor.darken(themeColor, step = 1, shade = 32);
            $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
            $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
        }
    }
}

6. Add a complimentary colour to our palette

Now we need to get the complimentary colour to our main colour. We do this by using xcolor’s complementary() method.

function colorGen() {
    // Create color thief
    var colorThief = new ColorThief();
    // Create image object from the image of choice.
    var sourceImage = $('#main-image')[0];

    /*=================================================================*/
    /*------------------ Set the primary colour -----------------------*/
    /*=================================================================*/
    // Get the dominant color of image
    var dominantColor = colorThief.getColor(sourceImage);
    // Loop to lighten the primary colour until it is as light as it can be whilst remaining readable on white. This gives it monet's 'pastel' look.
    var themeColor = dominantColor;
    if($.xcolor.readable(themeColor, '#fff') == true) {
        while ($.xcolor.readable(themeColor, '#fff') == true) {
            themeColor = $.xcolor.lighten(themeColor, step = 1, shade = 32);
            $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
            $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
        }
    } else {
        while ($.xcolor.readable(themeColor, '#fff') == false) {
            themeColor = $.xcolor.darken(themeColor, step = 1, shade = 32);
            $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
            $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
        }
    }

    /*=================================================================*/
    /*--------------- Set the complimentary colour --------------------*/
    /*=================================================================*/
    // Get the complimentary colour. this could instead be set to get triad, tetrad, analogous etc. if preferred.
    var complementary = $.xcolor.complementary( dominantColor );
}

7. Create more tints and shades

Now we create some tints and shades of the complimentary colour using xcolor’s lighten() and darken() methods. Starting with tints fist (because we want pastel colours if possible), we put them into an array called ‘compliments’. We then loop through that array until we find one that is readable() against the original dominant background colour.

function colorGen() {
    // Create color thief
    var colorThief = new ColorThief();
    // Create image object from the image of choice.
    var sourceImage = $('#main-image')[0];

    /*=================================================================*/
    /*------------------ Set the primary colour -----------------------*/
    /*=================================================================*/
    // Get the dominant color of image
    var dominantColor = colorThief.getColor(sourceImage);
    // Loop to lighten the primary colour until it is as light as it can be whilst remaining readable on white. This gives it monet's 'pastel' look.
    var themeColor = dominantColor;
    if($.xcolor.readable(themeColor, '#fff') == true) {
        while ($.xcolor.readable(themeColor, '#fff') == true) {
            themeColor = $.xcolor.lighten(themeColor, step = 1, shade = 32);
            $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
            $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
        }
    } else {
        while ($.xcolor.readable(themeColor, '#fff') == false) {
            themeColor = $.xcolor.darken(themeColor, step = 1, shade = 32);
            $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
            $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
        }
    }

    /*=================================================================*/
    /*--------------- Set the complimentary colour --------------------*/
    /*=================================================================*/
    // Get the complimentary colour. this could instead be set to get triad, tetrad, analogous etc. if preferred.
    var complementary = $.xcolor.complementary( dominantColor );
    // Make an array of shade / tints of the complementary colour
    compliments = [];
    // Tints first because we prefer them
    i = 0;
    while (i < 10) {
        compliments.push($.xcolor.lighten(complementary, step = i, shade = 32));
        i++;
    }
    console.log(compliments);
    //Then shades
    i = 0;
    while (i < 10) {
        compliments.push($.xcolor.darken(complementary, step = i, shade = 32));
        i++;
    }

    // loop through each tint/shade of complimenting colour in the array until it hits one that is legible against the primary background colour
    $(compliments).each(function(index, value) {
        var legible = $.xcolor.readable(value, dominantColor);
        console.log(legible);
        if (legible === true) {
            var complimentary = value;
            $('.complimentary-background-color').css('background-color', complimentary); // Giving any element the class of 'complimentary' will make the element background this colour.
            $('.complimentary-color').css('color', complimentary); // Giving any element the class of 'complimentary-color' will make the element text this colour.
            return false; // stop looping when one has passed
        }
    });     
}

8. Run the function

Now, all that’s left to do is add some javascript to  run the colorGen() function on page load, and when the select menu used in this demo is changed, although you won’t need that for your project.

// Run everything when the window has finished loading.
$(window).load(function() {
    colorGen();
});
// Run everything whenever the image changes (only needed for this demo)
$('#image-selector').on("input", changeImage);


// This powers the image switcher
function changeImage() {
    
    // Switch the SRC of the main image to match the select field
    myImage = $('#image-selector').val();
    $('#main-image').attr("src", myImage);

    // Don't do naything until the new SRC isloaded
    var tmpImg = new Image(); // create a temp image object
    tmpImg.src = $('#main-image').attr('src'); // load in the new src
    tmpImg.onload = function() { //run the colorGen() function when it's loaded.
        colorGen();
    };
}

The final code

Putting it all together into a single HTML file gives you the below.

<!doctype html>
<html class="no-js" lang="">
    <head>
        <meta name="robots" content="noindex">
        <meta charset="utf-8">
        <meta http-equiv="x-ua-compatible" content="ie=edge">
        <title>Dominant Color</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            * {
                font-family: sans-serif;
            }
        </style>
    </head>
    <body>
        <div style="margin: 2em auto; width: 300px;">
            <select id="image-selector" style="width: 100%; font-size: 1.25em; padding: 0.25em; border-radius: 0.25em;">
                <option value="img/image1.jpg">Image 1</option>
                <option value="img/image2.jpg">Image 2</option>
                <option value="img/image3.jpg">Image 3</option>
                <option value="img/image4.jpg">Image 4</option>
                <option value="img/image5.jpg">Image 5</option>
                <option value="img/image6.jpg">Image 6</option>
                <option value="img/image7.jpg">Image 7</option>
                <option value="img/image8.jpg">Image 8</option>
            </select>
        </div>
        <div id="container" style="width: 300px; height: 600px; margin: auto; position: relative; bordeR: 2px solid white;">
            <img id="main-image" src="img/image1.jpg" style="width: 100%;">
            <div class="primary-background-color" style="width: 100%; height: 1em; position: absolute; top: 0; left: 0; padding-top:0.25em;">
                <span style="font-size: 0.75em; color: white; display: block; float: left; padding-left: 0.5em;" ;>
                    14:27
                </span>
                <span style="float: right;">
                    <i class="fa fa-volume-down" style="font-size: 0.75em; display: inline-block; margin-right: 0.5em; color: white; float: right;"></i>
                    <i class="fa fa-wifi" style="font-size: 0.75em; display: inline-block; margin-right: 0.5em; color: white; float: right;"></i>
                    <i class="fa fa-battery-three-quarters" style="font-size: 0.75em; display: inline-block; margin-right: 0.5em; color: white; float: right;"></i>
                </span>
            </div>
            <div style="position: absolute; border-radius: 20px; background-color: white; padding: 10px; top: 30%; right: 20px;">
                <div class="primary-background-color" style="width: 15px; height: 100px; border-radius: 10px; margin: auto;">
                </div>
                <i class="fa fa-volume-down primary-color" style="font-size: 0.75em; display: block; text-align: center; padding-top: 0.5em;"></i>
            </div>
            <span class="complimentary-color" style="position: absolute; top: 40%; left: 20px; font-size: 1.5rem; max-width: 200px; transform: translateY(-50%); font-family: 'Raleway', sans-serif; font-weight: 600;">
                Wallpaper-based colour themes
            </span>
        </div>



        <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
        <script src="js/color-thief/dist/color-thief.min.js"></script>
        <script src="js/jquery.xcolor.js"></script>
        <script>
            function colorGen() {
                // Create color thief
                var colorThief = new ColorThief();
                // Create image object from the image of choice.
                var sourceImage = $('#main-image')[0];

                /*=================================================================*/
                /*------------------ Set the primary colour -----------------------*/
                /*=================================================================*/
                // Get the dominant color of image
                var dominantColor = colorThief.getColor(sourceImage);
                
                // Loop to lighten the primary colour until it is as light as it can be whilst remaining readable on white. This gives it monet's 'pastel' look.
                var themeColor = dominantColor;
                if($.xcolor.readable(themeColor, '#fff') == true) {
                    while ($.xcolor.readable(themeColor, '#fff') == true) {
                        themeColor = $.xcolor.lighten(themeColor, step = 1, shade = 32);
                        $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
                        $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
                    }
                } else {
                    while ($.xcolor.readable(themeColor, '#fff') == false) {
                        themeColor = $.xcolor.darken(themeColor, step = 1, shade = 32);
                        $('.primary-background-color').css('background-color', themeColor); // Giving any element the class of 'primary' will make that element background this colour.
                        $('.primary-color').css('color', themeColor); // Giving any element the class of 'primary-color' will make the element text this colour.
                    }
                }


                /*=================================================================*/
                /*--------------- Set the complimentary colour --------------------*/
                /*=================================================================*/
                // Get the complimentary colour. this could instead be set to get triad, tetrad, analogous etc. if preferred.
                var complementary = $.xcolor.complementary( dominantColor );
                // Make an array of shade / tints of the complementary colour
                compliments = [];
                // Tints first because we prefer them
                i = 0;
                while (i < 10) {
                    compliments.push($.xcolor.lighten(complementary, step = i, shade = 32));
                    i++;
                }
                console.log(compliments);
                //Then shades
                i = 0;
                while (i < 10) {
                    compliments.push($.xcolor.darken(complementary, step = i, shade = 32));
                    i++;
                }

                // loop through each tint/shade of complimenting colour in the array until it hits one that is legible against the primary background colour
                $(compliments).each(function(index, value) {
                    var legible = $.xcolor.readable(value, dominantColor);
                    console.log(legible);
                    if (legible === true) {
                        var complimentary = value;
                        $('.complimentary-background-color').css('background-color', complimentary); // Giving any element the class of 'complimentary' will make the element background this colour.
                        $('.complimentary-color').css('color', complimentary); // Giving any element the class of 'complimentary-color' will make the element text this colour.
                        return false; // stop looping when one has passed
                    }
                });                
            }

            // Run everything when the window has finished loading.
            $(window).load(function() {
                colorGen();
            });
            // Run everything whenever the image changes (only needed for this demo)
            $('#image-selector').on("input", changeImage);


            // This powers the image switcher
            function changeImage() {
                
                // Switch the SRC of the main image to match the select field
                myImage = $('#image-selector').val();
                $('#main-image').attr("src", myImage);

                // Don't do naything until the new SRC isloaded
                var tmpImg = new Image(); // create a temp image object
                tmpImg.src = $('#main-image').attr('src'); // load in the new src
                tmpImg.onload = function() { //run the colorGen() function when it's loaded.
                    colorGen();
                };
            }
        </script>
        <script src="https://kit.fontawesome.com/47f5690e4a.js" crossorigin="anonymous"></script>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Raleway:wght@600&display=swap" rel="stylesheet">
    </body>
</html>