Overview
Color Locking allows you to freeze specific colors in your palette, preventing them from changing during Smart Shuffle operations or automatic color derivations. This is essential for maintaining brand colors while exploring variations.
How to Lock Colors
Each color button in the theme customizer has a lock icon:
- Hover over any color button to reveal the lock icon
- Click the lock icon to toggle the lock state
- Locked colors show the icon permanently
- Unlocked colors hide the icon until hover
Lock Button UI
<button
onClick={(e) => {
e.stopPropagation();
onLockToggle(property);
}}
className={`absolute -top-2 -right-2 p-1 rounded-full border transition-opacity ${
isLocked ? "opacity-100" : "opacity-0 group-hover:opacity-100"
}`}
title={isLocked ? "Unlock color" : "Lock color"}
>
{isLocked ? (
<Lock size={9} strokeWidth={3.25} />
) : (
<Unlock size={9} strokeWidth={3.25} />
)}
</button>
The lock state is managed via a Set<string> in the component state:
const [lockedColors, setLockedColors] = useState<Set<string>>(new Set());
const handleLockToggle = (property: string) => {
setLockedColors((prev) => {
const newSet = new Set(prev);
if (newSet.has(property)) {
newSet.delete(property);
} else {
newSet.add(property);
}
return newSet;
});
};
You can lock any combination of colors - just your brand primary, all neutrals, or even the entire palette.
Lock Behavior During Shuffle
When you trigger a Smart Shuffle with locked colors:
const lockedColorValues: Record<string, string> = {};
Object.entries(theme.colors).forEach(([property, color]) => {
if (lockedColors.has(property)) {
lockedColorValues[property] = color;
}
});
2. Base Color Is Derived
The system tries to derive a base color from your locks:
let palette = generateColorPalette(
deriveBaseColorFromLocks(lockedColorValues, harmonyMode) || randomHex(),
themeName === "dark",
lockedColorValues, // ← Locked colors passed to generator
harmonyMode,
);
3. Only Unlocked Colors Update
Object.entries(palette).forEach(([property, color]) => {
if (!lockedColors.has(property)) {
updateThemeProperty(["colors", property], color);
}
});
Locked colors are preserved exactly - they won’t be nudged for contrast or adjusted in any way during shuffle.
Lock Behavior During Manual Edits
Locks also affect automatic color derivations when you manually change colors:
Preventing Color Picker on Locked Colors
const handleColorClick = (
color: string,
property: string,
button: HTMLButtonElement,
) => {
if (lockedColors.has(property)) return; // ← Locked colors can't be clicked
// ... open color picker
};
Clicking a locked color does nothing - you must unlock it first.
Border and Muted Derivation
When you change text or background, border and muted colors automatically update unless locked:
const currentText =
selectedProperty === "text" ? newColor : theme.colors.text;
const currentBg =
selectedProperty === "background" ? newColor : theme.colors.background;
if (!lockedColors.has("border")) {
const newBorder = mixOklch(currentText, currentBg, 0.82);
updateThemeProperty(["colors", "border"], newBorder);
}
if (!lockedColors.has("muted")) {
const newMuted = mixOklch(currentText, currentBg, 0.55);
updateThemeProperty(["colors", "muted"], newMuted);
}
Locking border or muted preserves their exact values.
On-Color Derivation
const onColorMap: Record<string, string> = {
primary: "onPrimary",
container: "onContainer",
accent: "onAccent",
success: "onSuccess",
error: "onError",
warning: "onWarning",
};
if (onColorMap[selectedProperty]) {
updateThemeProperty(
["colors", onColorMap[selectedProperty]],
pickOnColor(newColor),
);
}
Note: On-colors are always derived from their parent color, regardless of locks. This ensures text remains readable on colored surfaces.
You cannot directly lock on-colors (onPrimary, onContainer, etc.) - they are always auto-calculated from their parent.
Lock Behavior During Mode Toggle
When switching between light and dark modes, locked colors are preserved:
const toggleTheme = () => {
const targetIsDark = themeName !== "dark";
const targetMode = targetIsDark ? "dark" : "light";
const adapted = adaptColorsForMode(
theme.colors as Record<string, string>,
targetIsDark,
lockedColors // ← Locks passed to adaptation
);
setTheme(targetMode, adapted as Partial<typeof theme.colors>);
};
Inside adaptColorsForMode:
for (const [role, l] of Object.entries(targetL)) {
if (lockedColors.has(role)) {
adapted[role] = currentColors[role]; // ← Keep exact value
} else {
const hue = chroma(currentColors[role]).oklch()[2] || 0;
adapted[role] = chroma.oklch(l, safeChroma(l, hue, vibrancyMap[role]), hue).hex();
}
}
Locked colors maintain their exact hex values even when switching modes. This can be useful but may cause accessibility issues if the locked color doesn’t have enough contrast in the new mode.
When locking colors for use in both light and dark modes, ensure they have sufficient contrast against both light and dark backgrounds.
Smart Lock Strategies
Brand Color Preservation
Lock your brand primary and shuffle to explore complementary accents:
lockedColors = new Set(["primary"]);
The shuffle will derive a base color from your primary’s hue, ensuring harmony.
Neutral Palette Lock
Lock text, background, and container to preserve your neutral palette:
lockedColors = new Set(["text", "background", "container"]);
Shuffle will only vary accent and primary colors.
Single Accent Lock
Lock just the accent to explore different primary colors that complement it:
lockedColors = new Set(["accent"]);
In complementary mode, the primary will be ~180° away from your accent.
State Color Lock
Lock success, error, and warning if you have standard semantic colors:
lockedColors = new Set(["success", "error", "warning"]);
Lock Persistence
Lock state is stored only in React component state - it is not persisted to localStorage or URL. This is intentional:
- Locks are a workflow tool, not part of the theme
- Shared URLs shouldn’t enforce locks on recipients
- Each session starts with all colors unlocked
If you close the page and return, all locks will be cleared. Lock states are session-only.
Visual Feedback
Locked State
- Lock icon always visible
- Slightly darker border on hover (indicating interactivity)
- Tooltip: “Unlock color”
Unlocked State
- Lock icon hidden by default
- Lock icon appears on hover with transition
- Tooltip: “Lock color”
Disabled Interaction
- Locked color buttons don’t open color picker when clicked
- Visual state remains the same (no disabled styling)
Related Features