Try It Live
themeColor value updates multiple parts of the page so your app has a consistent visual style.
What It Updates
- Hero background gradient in
#heroDiv - Hero text colors (
h1andp) - Masthead icon color for
#mastheadIcon - Could be refactored to include any other element you choose to theme
High-Level Flow
- Create/select one
themeColorhex value (for example from a color picker). - Convert that color to formats needed by CSS/canvas (hex and RGB).
- Apply RGB values to gradient CSS for the hero section.
- Apply hex color to hero text styles.
- 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:
(intVal >> 16) & 255: move the red byte into the rightmost position, then keep only that byte.(intVal >> 8) & 255: move the green byte into the rightmost position, then keep only that byte.intVal & 255: blue is already in the rightmost position, so keep only that byte.
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');
themeHexis the selected theme color, such as#4c25e1.heroDivandmastheadIconare references to the two page elements we want to update.- Getting these once at the top avoids repeating
document.getElementById(...).
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; });
}
if (heroDiv)is a safety check so we only run this block if that element exists.hexToRgb(themeHex)converts hex into numbers we can use inrgba(...).- The gradient uses the same RGB color twice with different alpha values (
0.26and0.16) for depth. - The image pattern is layered underneath the gradient so the hero keeps texture.
querySelectorAll('h1, p')finds hero heading and paragraph text, then applies the same theme color to both.
Early return if icon is missing
if (!mastheadIcon) return;
- If the icon is not on this page, stop here.
- This prevents canvas/image code from running when there is no icon to tint.
The dataset step (important)
const source = mastheadIcon.dataset.originalSrc || mastheadIcon.src;
mastheadIcon.dataset.originalSrc = source;
datasetstores custom data directly on an HTML element usingdata-*attributes.dataset.originalSrcremembers the icon's true original file URL the first time we run.- Why this matters: after tinting,
mastheadIcon.srcbecomes a generated data URL, not the original file. - Without this cache, recoloring repeatedly could keep tinting an already tinted icon and degrade quality.
- With this cache, every recolor starts from the same clean source image, so results stay crisp and predictable.
Load the image and wait until it is ready
const srcImg = new Image();
srcImg.onload = () => {
// canvas tinting work happens here
};
srcImg.src = source;
new Image()creates an image object in JavaScript memory.onloadruns only after the image is fully available to draw.- Setting
srcImg.src = sourcestarts loading.
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);
- This canvas is not added to the page; it is a temporary drawing workspace.
- Using natural image dimensions helps preserve original resolution.
clearRectensures a clean pixel buffer.drawImagepaints the original icon onto the canvas first.
The globalCompositeOperation step (important)
ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = themeHex;
ctx.fillRect(0, 0, offscreen.width, offscreen.height);
ctx.globalCompositeOperation = 'source-over';
globalCompositeOperationcontrols how new paint combines with pixels already on canvas.'source-atop'means: apply the new fill only where existing icon pixels already exist.- Result: the color fill tints the icon shape, but does not paint the transparent background around it.
fillRect(...)draws one full-canvas color layer; compositing mode clips it to icon pixels.- Resetting to
'source-over'returns canvas behavior to normal for any future drawing. - If you forget to reset, later draw calls can blend unexpectedly and cause hard-to-debug visual artifacts.
Export tinted icon and show it on the page
mastheadIcon.src = offscreen.toDataURL('image/png');
}
toDataURL('image/png')converts the canvas pixels to a PNG data URL string.- Assigning that string to
mastheadIcon.srcupdates the icon instantly in the browser. - The disk file is never changed; only the in-page displayed image is replaced.
themeHex input.
Process map (from click to recolored icon)
Beginner Tips
- Use one source of truth for color: this page uses the value from
#themeColorPicker. - When the user clicks
Apply Theme, pass that picker value directly intoapplyThemeColor(themeHex). - Keep all theming changes in one function so every themed element updates together.
- If you add a new themed element, place its style update in
applyThemeColor(themeHex)so it stays synchronized. - Always test very light and very dark colors to confirm text contrast remains readable.