Applying A Theme Color

Apply a Theme Color to Selected Page Elements

Try It Live

What It Updates

High-Level Flow

  1. Create/select one themeColor hex value (for example from a color picker).
  2. Convert that color to formats needed by CSS/canvas (hex and RGB).
  3. Apply RGB values to gradient CSS for the hero section.
  4. Apply hex color to hero text styles.
  5. Repaint the icon using an offscreen canvas and the same color.

Annotated Example

function hexToRgb(hex) {
    const clean = hex.replace('#', '');
    const full = clean.length === 3
        ? clean.split('').map(ch => ch + ch).join('')
        : clean;
    const intVal = parseInt(full, 16);
    return {
        r: (intVal >> 16) & 255,
        g: (intVal >> 8) & 255,
        b: intVal & 255
    };
}//end hexToRgb

function applyThemeColor(themeHex) {
    const heroDiv = document.getElementById('heroDiv');
    const mastheadIcon = document.getElementById('mastheadIcon');

    if (heroDiv) {
        const rgb = hexToRgb(themeHex);
        heroDiv.style.backgroundImage =
            `linear-gradient(
                135deg,
                rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.26) 0%,
                rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.16) 100%
            ), ` +
            `url("images/whiteStarDiamondPattern.png")`;

        const heroText = heroDiv.querySelectorAll('h1, p');
        heroText.forEach(el => { el.style.color = themeHex; });
    }

    if (!mastheadIcon) return;

    const source = mastheadIcon.dataset.originalSrc || mastheadIcon.src;
    mastheadIcon.dataset.originalSrc = source;

    const srcImg = new Image();
    srcImg.onload = () => {
        const offscreen = document.createElement('canvas');
        offscreen.width = srcImg.naturalWidth || srcImg.width;
        offscreen.height = srcImg.naturalHeight || srcImg.height;

        const ctx = offscreen.getContext('2d');
        if (!ctx) return;

        ctx.clearRect(0, 0, offscreen.width, offscreen.height);
        ctx.drawImage(srcImg, 0, 0);
        ctx.globalCompositeOperation = 'source-atop';
        ctx.fillStyle = themeHex;
        ctx.fillRect(0, 0, offscreen.width, offscreen.height);
        ctx.globalCompositeOperation = 'source-over';

        mastheadIcon.src = offscreen.toDataURL('image/png');
    };
    srcImg.src = source;
}//end applyThemeColor

Unpacking hexToRgb(hex)

The color picker gives a hex value such as #4c25e1. Hex is compact and easy to store, but our gradient uses rgba(...), which needs separate red, green, and blue numbers.

The hexToRgb(hex) function converts that one hex string into an object like { r: 76, g: 37, b: 225 }. Then those numbers are inserted into the gradient string so we can also set transparency using the alpha value.

A key step is const full = .... This handles both common hex formats: 6-digit values like #4c25e1 and 3-digit shorthand like #abc. If the value is 3 digits, each character is doubled (abc -> aabbcc) so the rest of the function can always work with a full 6-digit color string.

The return statement sends back an object with three number properties: r, g, and b. Each line extracts one color channel from the full hex value. For beginners, you can think of this as "cutting" one packed color number into three parts we can use directly in rgba(...).

Here is what each expression does in plain language:

The number 255 is 0xFF in hex (binary 11111111), which acts as a mask to keep exactly one color byte.

Worked example: If the user picks #4c25e1, then hexToRgb("#4c25e1") returns { r: 76, g: 37, b: 225 }. That means your gradient can use values like rgba(76, 37, 225, 0.26) and rgba(76, 37, 225, 0.16).

In short, this function is a translator between user-friendly picker output and the numeric format needed to build a themed gradient.

A String-Slicing Alternative

The bitwise version of hexToRgb uses bit-shifting (>>) and masking (&) to extract color channels from one large integer. A beginner-friendlier approach is to treat the hex string as text and slice out each two-character pair directly.

Original — bitwise

function hexToRgb(hex) {
    const clean = hex.replace('#', '');
    const full = clean.length === 3
        ? clean.split('').map(ch => ch + ch).join('')
        : clean;
    const intVal = parseInt(full, 16);
    return {
        r: (intVal >> 16) & 255,
        g: (intVal >> 8)  & 255,
        b: intVal          & 255
    };
}//end hexToRgb

Alternative — string slicing

function hexToRgbAlternative(hex) {
    hex = hex.replace('#', '');

    // Expand 3-digit shorthand to 6 digits
    if (hex.length === 3) {
        hex = hex[0] + hex[0]
            + hex[1] + hex[1]
            + hex[2] + hex[2];
    }

    // Slice each pair and convert base-16 → decimal
    let r = parseInt(hex.slice(0, 2), 16);
    let g = parseInt(hex.slice(2, 4), 16);
    let b = parseInt(hex.slice(4, 6), 16);

    return { r, g, b };
}//end hexToRgbAlternative

Both functions produce identical results. Here is how the two techniques compare:

Step Bitwise version String-slicing version
Strip # hex.replace('#', '') hex.replace('#', '')
Expand 3-digit shorthand clean.split('').map(ch => ch + ch).join('') hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]
Parse entire hex string parseInt(full, 16) — one call, one integer Not needed — each pair is parsed separately
Extract red channel (intVal >> 16) & 255 parseInt(hex.slice(0, 2), 16)
Extract green channel (intVal >> 8) & 255 parseInt(hex.slice(2, 4), 16)
Extract blue channel intVal & 255 parseInt(hex.slice(4, 6), 16)
Return value { r, g, b } { r, g, b }

Unpacking applyThemeColor(themeHex)

This function takes one color (for example #4c25e1) and applies it to multiple parts of the page. It themes text and background in the hero area, then tints the icon by drawing a recolored copy on an offscreen canvas.

Function setup and element lookup

function applyThemeColor(themeHex) {
    const heroDiv = document.getElementById('heroDiv');
    const mastheadIcon = document.getElementById('mastheadIcon');

Theme the hero section with RGB + CSS

if (heroDiv) {
    const rgb = hexToRgb(themeHex);
    heroDiv.style.backgroundImage =
        `linear-gradient(135deg,
            rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.26) 0%,
            rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.16) 100%), ` +
        `url("images/whiteStarDiamondPattern.png")`;

    const heroText = heroDiv.querySelectorAll('h1, p');
    heroText.forEach(el => { el.style.color = themeHex; });
}

Early return if icon is missing

if (!mastheadIcon) return;

The dataset step (important)

const source = mastheadIcon.dataset.originalSrc || mastheadIcon.src;
mastheadIcon.dataset.originalSrc = source;

Load the image and wait until it is ready

const srcImg = new Image();
srcImg.onload = () => {
    // canvas tinting work happens here
};
srcImg.src = source;

Create an offscreen canvas and draw original icon

const offscreen = document.createElement('canvas');
offscreen.width = srcImg.naturalWidth || srcImg.width;
offscreen.height = srcImg.naturalHeight || srcImg.height;

const ctx = offscreen.getContext('2d');
if (!ctx) return;

ctx.clearRect(0, 0, offscreen.width, offscreen.height);
ctx.drawImage(srcImg, 0, 0);

The globalCompositeOperation step (important)

ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = themeHex;
ctx.fillRect(0, 0, offscreen.width, offscreen.height);
ctx.globalCompositeOperation = 'source-over';

Export tinted icon and show it on the page

mastheadIcon.src = offscreen.toDataURL('image/png');
}

Process map (from click to recolored icon)

User picks hex color applyThemeColor(themeHex) Find heroDiv + mastheadIcon
DOM styling path
hexToRgb(themeHex) Set gradient + text color
Canvas tinting path
dataset caches originalSrc new Image() loads source source-atop tint on offscreen canvas toDataURL updates icon src

Beginner Tips