Color Format Converter: Code Examples

Pick or type a color and convert instantly between HEX, RGB, HSL, HSV, OKLCH, and CSS named colors.

ZERO UPLOAD · ALL LOCAL
  1. Drag the crosshair on the gradient canvas to pick a color, or type directly into any format field to convert it instantly.
  2. Use the hue slider to change the color family and the opacity slider (or Opacity % field) to adjust transparency.
  3. The CSS Named field shows the closest CSS color name to your pick. The small swatch beside it shows what that named color actually looks like.
  4. The Contrast Check section shows the WCAG AA / AAA rating for black text and white text on your color.
  5. Click Copy next to any field to copy that format value to your clipboard.
Hue
Opacity
Invalid format

Contrast Check

Text
Black on color
Text
White on color

CSS Color Level 4 Color Guide

CSS Color Level 4 changes what colors are expressible in a stylesheet. The specification adds oklch(), color(), color-mix(), and relative color syntax. These functions handle wide-gamut displays, perceptually uniform scales, and dynamic color derivation. All modern browsers support these features as of 2023. Understanding which function to use for which purpose is the practical guide to adopting Level 4 color in production CSS today.1

New color functions in CSS Color Level 4

CSS Color Level 4 introduces four key additions beyond the legacy rgb(), hsl(), and hex formats. oklch(L C H) expresses color in a perceptually uniform polar space. color(display-p3 r g b) accesses the wider gamut of P3-capable displays. color-mix(in oklch, color1 percentage, color2) blends two colors in any color space. Relative color syntax allows deriving a new color from an existing one by modifying individual channels: color(from blue oklch calc(l + 0.1) c h) produces a lighter blue without JavaScript. Each of these additions solves a specific problem that legacy CSS color formats could not address without scripting.2

Converting from HEX and RGB to Level 4 formats

Migrating to oklch() starts with converting existing HEX or RGB design tokens to OKLCH coordinates. This tool produces the oklch() value for any HEX or RGB input. The three OKLCH coordinates can then be used in CSS custom properties: --color-brand: oklch(0.960 0.241 128.2). Consequently, the design token retains full perceptual meaning. The lightness, chroma, and hue are explicit rather than being opaque base-16 pairs. Existing sRGB values in oklch() remain within the sRGB gamut and render identically to their HEX equivalents on any display, making the migration non-breaking. Designers can update tokens incrementally without rewriting every component that references them.

Practical compatibility and fallbacks

Chrome 111+, Firefox 113+, Safari 15.4+, and Edge 111+ support all CSS Color Level 4 color functions. Coverage exceeds 90% of global browser traffic. For the remaining 10%, primarily Internet Explorer and very old mobile browsers, use @supports (color: oklch(0 0 0)) to apply Level 4 styles conditionally, with HEX fallbacks declared first: --color-brand: #c8ff00; --color-brand: oklch(0.960 0.241 128.2);. The PostCSS plugin postcss-oklch can automate these fallbacks at build time. Furthermore, color(display-p3 …) degrades gracefully to the nearest sRGB color on non-P3 displays, so wide-gamut colors can be used as progressive enhancement without testing on every device.1 This combination of @supports and PostCSS tooling makes adoption practical today.

Practical color-mix() patterns for UI components

color-mix() solves a problem that previously required preprocessors or manual calculation: deriving tints, shades, and tones from a base color. For a button hover state, color-mix(in oklch, var(--color-primary) 85%, black) produces a slightly darker version of the base in perceptual space. For a disabled state, color-mix(in oklch, var(--color-primary) 40%, transparent) creates a semi-transparent wash that blends with whatever background it sits on. These expressions replace hard-coded secondary tokens for variants that are derivable from a single base, reducing the number of custom properties in your token set.

Avoiding common color-mix() pitfalls

Blending in sRGB with color-mix(in srgb, ...) produces hue shifts that are perceptually wrong: mixing blue and yellow in sRGB gives a desaturated greenish grey instead of the perceptual midpoint. Always specify the color space explicitly, and prefer in oklch or in lch for any blend where perceptual accuracy matters. The only time in srgb is appropriate is when you are matching a specific sRGB output that was produced by a tool that blends in sRGB (like Photoshop's layer blending).

The explicit space also affects opacity handling, because the interpolation math differs between sRGB and OKLCH when a transparent endpoint is involved. A mix toward transparent in OKLCH stays in the perceptual hue family, while the same mix in sRGB can drift through a greyed midpoint that looks washed out. Naming the space on every color-mix() call removes that ambiguity, so the blend you write is the blend you get regardless of which browser computes it.3

Wide-gamut color() syntax and display-p3 adoption

The color() function unlocks colors that cannot be expressed in HEX or rgb(). Writing color(display-p3 0.5 1 0) specifies a lime green that extends beyond the sRGB boundary into the P3 gamut. On a P3-capable display (iPhone, iPad Pro, MacBook Pro, many Android flagships), this renders as a more vivid lime than any HEX value can produce. On an sRGB display, the browser clamps to the nearest in-gamut color. This graceful degradation makes P3 colors safe to use as progressive enhancement: they improve the experience on capable displays without breaking anything on standard ones.

Detecting P3 support in CSS and JavaScript

The CSS media query @media (color-gamut: p3) matches on devices that support at least the P3 gamut. Use it to conditionally apply P3 colors: @media (color-gamut: p3) { --color-accent: color(display-p3 0.5 1 0); }. In JavaScript, the window.matchMedia('(color-gamut: p3)') MediaQueryList object tells you the same information, allowing you to adjust canvas rendering or image selection based on display capability. Most Apple devices from 2017 onward and many Android flagships from 2020 onward report P3 support.4

Migrating an existing design system to CSS Color Level 4

Migrating a legacy HEX-based design system to CSS Level 4 does not require a big-bang rewrite. Start by converting your primitive tokens (raw color values) from HEX to oklch() while keeping the semantic token names (--color-primary, --color-surface) unchanged. Components that reference semantic tokens continue to work without modification. Once primitives are in oklch(), add derived tokens using relative color syntax for hover, active, and disabled states. Finally, introduce color() for any P3-enhanced accent colors. Each step is independently reversible and testable.

PostCSS tooling for gradual migration

postcss-preset-env with its color-function and oklab-function plugins transforms Level 4 syntax into HEX fallbacks during the build, letting you write modern CSS while supporting older browsers. Configure it with a browserslist target that matches your project's support matrix. For teams using Tailwind v4, this step is unnecessary because Tailwind's generated CSS already uses oklch() natively, and you add postcss-preset-env to the pipeline to handle fallback generation for the Tailwind output.2

When to use this

Use this reference when starting a new CSS design system, migrating existing HEX tokens to oklch(), or adding wide-gamut color support to a site that targets modern browsers.

Notes

oklch() stays within the sRGB gamut by default. Using oklch() alone does not add P3 colors. Wide gamut requires color(display-p3 …) with chroma values above the sRGB boundary. The color() function general syntax is distinct from oklch(): color(srgb 0.8 0.5 0) and rgb(204, 128, 0) represent the same color differently.

Examples

Design token in oklch()

--color-brand: oklch(0.960 0.241 128.2);

In :root, replaces --color-brand: #c8ff00

Wide-gamut lime (P3)

color(display-p3 0.49 1 0)

More vivid than #7dff00; only renders wider on P3 displays

color-mix blend

color-mix(in oklch, oklch(0.612 0.212 261.5) 70%, white)

Produces a lighter tint of blue-500 in perceptual space

Verify with the Color Format Converter tool.

Design token in oklch()

--color-brand: oklch(0.960 0.241 128.2);

In :root, replaces --color-brand: #c8ff00

Sources
  1. 1.

    "CSS color() function," caniuse.com, accessed June 2026. https://caniuse.com/css-color-function

  2. 2.

    W3C, "CSS Color Module Level 4," w3.org, June 2026. https://www.w3.org/TR/css-color-4/

  3. 3.

    Mozilla Developer Network, "color-mix() CSS function," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix

  4. 4.

    Mozilla Developer Network, "color() CSS function," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color

FAQ

Tailwind CSS v4 OKLCH Colors

Tailwind CSS v4 defines its entire built-in color palette in OKLCH.1 Every shade from slate-50 to red-950 is expressed as an oklch() coordinate, replacing the HEX values used in Tailwind v3. This change makes the palette perceptually uniform: shade levels at the same numeric step (e.g., -500) look consistently similar in perceived brightness across all hue families.2 Understanding how to add custom brand colors in the same OKLCH format keeps your theme consistent with the built-in palette.

How Tailwind v4 uses OKLCH

Tailwind v4 configuration lives in CSS rather than a JavaScript config file. Custom colors are defined in a @theme { } block using CSS custom properties: --color-brand: oklch(0.960 0.241 128.2). Tailwind generates background-color, text-color, border-color, and other utilities for any custom property in the @theme block.3 The three OKLCH channels map to the same dimensions Tailwind uses for built-in palette construction. L controls the shade level, C controls the saturation, and H controls the hue family. A custom brand color defined in OKLCH integrates visually with the system rather than standing out as an inconsistency. This seamless integration is what makes OKLCH the natural choice for Tailwind v4 theming, because your brand colors and built-in palette shades share the same perceptual model.

Converting brand colors to OKLCH for Tailwind v4

The most common starting point is a brand HEX from a style guide or Figma design. Converting that HEX to OKLCH gives the three coordinates needed for the @theme block, and this conversion is the essential first step because Tailwind v4 expects OKLCH values in the @theme block rather than HEX.

Mapping OKLCH coordinates to Tailwind shade levels

The L value shows how the brand color maps onto the Tailwind shade scale. L near 0.95 corresponds to the -50 shade level, and L near 0.20 corresponds to the -900 level. Knowing the L helps decide which Tailwind shade the brand color should replace or sit alongside. The C value shows how saturated the color is relative to the Tailwind palette, and H shows its hue family position.

The coordinate reading also guards against accidental palette clashes. If your brand L lands near an existing shade level, you can deliberately place it one step apart so the two never read as the same tone in a single UI. That small spacing is easier to reason about from OKLCH numbers than from HEX pairs, because the L channel tells you the exact lightness relationship at a glance rather than buried inside three hex digits.

Generating a full custom palette in OKLCH

A complete custom brand palette for Tailwind v4 requires ten shade steps (50 through 950). The OKLCH approach holds H constant for the entire palette, keeping the hue family fixed, and adjusts L and C together to match the visual weight of equivalent Tailwind built-in shades. The dark shades (900-950) lower L toward 0.20-0.30 and reduce C slightly. The light shades (50-100) raise L toward 0.93-0.97 and lower C significantly. The exact C curve depends on the hue: yellow and cyan hues have different maximum chroma at the same L level, requiring per-hue calibration rather than a single formula.4

Migrating a Tailwind v3 palette to v4 OKLCH

Tailwind v3 stored colors as HEX in tailwind.config.js; Tailwind v4 stores them as oklch() in CSS.1 Migrating requires converting every custom color from HEX to OKLCH and moving the definitions from JavaScript to a @theme block. The CapyToolkit color converter produces the oklch() coordinate for each HEX value in your v3 config. Paste each HEX, copy the OKLCH output, and write the @theme entry: --color-brand: oklch(0.612 0.212 261.5). This manual approach works for small palettes; for large ones, a script that reads your v3 config and writes the v4 @theme block automates the conversion.

Preserving utility class names during migration

Tailwind v4 generates the same utility class names from custom properties as v3 did from its JavaScript config. A color defined as --color-brand in @theme produces bg-brand, text-brand, border-brand, and every other utility that v3 produced from a brand color in tailwind.config.js. Your HTML does not need to change during the migration: classes like bg-brand and text-brand continue to work. The only visible difference is that the generated CSS uses oklch() instead of HEX, which modern browsers render identically for sRGB-gamut colors.

Advanced Tailwind v4 color configuration patterns

Tailwind v4 supports color opacity modifiers (bg-brand/50) for any color defined in @theme, but the implementation differs from v3. Instead of generating separate RGBA utilities, v4 uses CSS color-mix() to blend the color with transparent at the specified percentage and applies the opacity at the point of use.5 This means bg-brand/50 works even when --color-brand is defined in oklch(), because the opacity is applied by the browser's color compositor rather than by a precomputed RGBA value. The result is more accurate rendering, especially for colors near the sRGB gamut boundary.

Defining color families with CSS @theme

For a complete brand palette, define each shade as a separate custom property in @theme: --color-brand-50, --color-brand-100, through --color-brand-950. Tailwind generates utilities for each: bg-brand-50, text-brand-500, border-brand-900, and so on. The OKLCH L values for each step should follow Tailwind's built-in lightness curve for visual consistency. If your brand color's natural lightness sits at the 500 level, derive lighter shades by increasing L and darker shades by decreasing L, matching the spacing Tailwind uses for its slate or gray palette.

When to use this

Use this when adding a custom brand color to a Tailwind CSS v4 project, migrating from a Tailwind v3 HEX-based theme to v4 OKLCH tokens, or generating a perceptually consistent custom shade scale.

Notes

Tailwind v4 uses oklch() natively. No PostCSS plugin is needed for oklch() support in the generated CSS. The @theme block replaces tailwind.config.js for color customization. Utility classes for custom properties in @theme are auto-generated: --color-brand generates bg-brand, text-brand, border-brand, etc.

Examples

Single brand color in @theme

@theme {
  --color-brand: oklch(0.960 0.241 128.2);
}

Generates bg-brand, text-brand, etc.

Shade scale entry (brand-500)

--color-brand-500: oklch(0.612 0.212 261.5);

Match the L value to the Tailwind built-in shade at that level

HEX fallback for IE support

--color-brand: #c8ff00;
--color-brand: oklch(0.960 0.241 128.2);

IE reads the first value; modern browsers use oklch()

Verify with the Color Format Converter tool.

Single brand color in @theme

@theme {
  --color-brand: oklch(0.960 0.241 128.2);
}

Generates bg-brand, text-brand, etc.

Sources
  1. 1.

    Adam Wathan, "Tailwind CSS v4.0," tailwindcss.com, January 2025. https://tailwindcss.com/blog/tailwindcss-v4

  2. 2.

    CSS Working Group, "CSS Color Module Level 4," W3C, June 2026. https://www.w3.org/TR/css-color-4/

  3. 3.

    Tailwind CSS, "Theme Variables," tailwindcss.com, accessed June 2026. https://tailwindcss.com/docs/theme

  4. 4.

    Andrey Sitnik and Travis Turner, "OKLCH in CSS: Why We Moved from RGB and HSL," evilmartians.com, September 2025. https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl

  5. 5.

    Mozilla, "color-mix()," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix

FAQ

Figma to CSS Color Workflow

Figma exports colors as HEX by default.1 CSS increasingly favors oklch().2

Figma's fill panel shows colors in HEX, RGB, or HSL depending on the picker mode, but its "Copy as CSS" function always outputs HEX for solid fills. When a design system uses OKLCH or HSL custom properties in CSS, a developer receiving a HEX value from Figma must convert it before it can be used in the token system. Understanding Figma's color model and its export behavior eliminates ambiguity about where to convert and what precision to expect.

How Figma stores colors internally

Figma stores all colors in its internal data model as linear light RGB values with floating-point precision, not as the gamma-corrected HEX that appears in the UI.1 When you see #3b82f6 in the Figma fill panel, Figma has converted from its internal linear representation to the sRGB-gamma-corrected integer triplet. This conversion step is where most handoff confusion begins.

Why Figma's internal model matters for handoff

Colors copied as HEX from Figma are always in the standard sRGB gamut, making them safe to use in any CSS color function that operates in sRGB, including HEX, rgb(), hsl(), and oklch() for standard web colors. This internal representation is why Figma can display colors accurately across different color profiles without losing precision. For developers receiving Figma colors, the key takeaway is that every HEX value is already sRGB-safe and ready for CSS use.

The linear-to-sRGB step is also why Figma colors stay consistent when moved between tools. Because the export always lands in standard sRGB, a HEX you copy behaves the same in a CSS stylesheet, a design token file, or a build script. That predictable output is what lets a team treat Figma as the single source of truth for color without worrying that the numbers will shift depending on which tool reads them next.

Exporting Figma colors to CSS formats

Figma's "Copy as CSS" copies a fill as #rrggbb. The fill panel also accepts HEX, RGB, and HSL input. Figma does not currently export oklch() or color(display-p3 …) natively from its standard fill panel. These formats require a plugin like "OKLCH Color" or "CSS Variables" to inspect or copy. Figma variables (the design tokens feature) can be configured to export in multiple formats via the Figma REST API, but the default copy-paste workflow always goes through HEX. The practical workflow is to copy HEX from Figma and then convert to oklch() using this tool before writing the CSS custom property.

Keeping design tokens in sync

Design systems that define tokens in both Figma (as Figma variables) and code (as CSS custom properties) need a reliable sync strategy. The simplest approach stores the canonical value in HEX, matching what Figma exports, and derives oklch() or HSL at build time. A more robust approach uses Figma's REST API to pull variable values programmatically and converts them to oklch() as part of a design token pipeline (Style Dictionary, Theo, or a custom script). Any sync approach must account for Figma's floating-point precision. The HEX it displays may differ by 1 in one channel from the HEX you would independently compute from the same color definition due to rounding in Figma's display layer.3

Figma variables and multi-mode color tokens

Figma variables (released in 2023) support multiple modes per collection, which maps directly to light and dark theme tokens.4 A color variable named color/primary can have mode "Light" set to #3b82f6 and mode "Dark" set to #60a5fa. When a developer exports these variables via the Figma REST API, they receive a JSON structure with the mode values. The conversion pipeline then transforms each mode's HEX to OKLCH and writes separate @theme blocks: @media (prefers-color-scheme: dark) { :root { --color-primary: oklch(0.72 0.15 261.5); } }. This approach keeps the design intent (same semantic token, different values per mode) intact across the design-to-code boundary.

Using Figma's REST API for automated token sync

The Figma REST API endpoint GET /v1/files/:file_key/variables/local returns every variable in a file as a structured JSON response with resolved values per mode. A scheduled CI job that polls this endpoint, converts the HEX values to OKLCH, and writes a CSS file with @theme blocks automates the design token sync. Tools like Tokens Studio for Figma format their variable output for direct consumption by Style Dictionary or similar token compilers. Running this sync on every design system branch merge ensures the CSS tokens stay current without manual copy-paste.

Figma plugins for OKLCH color inspection

Figma does not natively display OKLCH coordinates in its fill panel, but several plugins fill this gap. The "OKLCH Color" plugin adds an inspector panel that shows the OKLCH coordinates of the currently selected fill, and it lets you input OKLCH values directly to set a fill. The "CSS Variables" plugin reads Figma variables and exports them in CSS custom property syntax, including oklch() output. For teams that design in Figma and implement in Tailwind v4, these plugins bridge the gap: the designer works in HEX (Figma's native format) while the developer reads OKLCH values from the plugin overlay.

Previewing P3 colors in Figma

Figma renders colors in the display's native color space, which on a P3-capable Mac means colors that extend beyond the sRGB boundary appear more vivid in Figma than they will on an sRGB browser.5 To preview sRGB-safe colors accurately, set the file's color profile to sRGB via Figma menu > Preferences > Color profile. This ensures the HEX values you copy from Figma look the same in a browser as they did in the design tool, eliminating the "it looked different in Figma" handoff problem.

When to use this

Use this when setting up a design-to-code handoff that needs colors to match precisely between Figma and a CSS token system, particularly when the CSS system uses oklch() or HSL and Figma exports HEX.

Notes

Figma exports HEX for solid fills via "Copy as CSS". There is no built-in oklch() export. Figma's native color picker does not show OKLCH coordinates; use a plugin for that. Color variables in Figma can be exported via the Figma REST API as raw floating-point values for more precise conversion.

Examples

Figma HEX → CSS token

Figma: #3b82f6
CSS: --color-primary: oklch(0.612 0.212 261.5);

Convert via this tool; paste oklch() into CSS

Figma HSL picker → HSL custom property

Figma: H 217, S 91%, L 60%
CSS: --color-primary: hsl(217, 91%, 60%);

HSL values copy directly when Figma picker is in HSL mode

HEX fallback + oklch token

--color-primary: #3b82f6;
--color-primary: oklch(0.612 0.212 261.5);

Older browsers use HEX; modern browsers use oklch()

Verify with the Color Format Converter tool.

Figma HEX → CSS token

Figma: #3b82f6
CSS: --color-primary: oklch(0.612 0.212 261.5);

Convert via this tool; paste oklch() into CSS

Sources
  1. 1.

    Figma, "RGB/RGBA," developers.figma.com, accessed June 2026. https://developers.figma.com/docs/plugins/api/RGB/

  2. 2.

    Adam Argyle, "The Expanding Gamut of Color on the Web," css-tricks.com, 2021. https://css-tricks.com/the-expanding-gamut-of-color-on-the-web/

  3. 3.

    Design Tokens Community Group, "Legacy color," github.com, 2023. https://github.com/design-tokens/community-group/issues/137

  4. 4.

    Dylan Field, "Config 2023: Reimagining Where Teams Design and Build Together," figma.com, June 2023. https://www.figma.com/blog/config-2023-recap/

  5. 5.

    Dean Jackson, "Improving Color on the Web," webkit.org, July 2016. https://webkit.org/blog/6682/improving-color-on-the-web/

FAQ

CSS Custom Properties for Color Theming

CSS custom properties are the standard mechanism for design token color systems.1

Defining --color-primary: oklch(0.612 0.212 261.5) in :root makes the color available to every element in the document via var(--color-primary). Changing that single custom property updates every element that references it. The change can come from JavaScript, a data-theme attribute, or a media query. This cascade-driven pattern is how modern CSS design systems implement dark mode, white-labeling, and component-level overrides without class proliferation.

Defining a color token system with custom properties

A CSS color token system typically defines primitive tokens (raw color values) and semantic tokens (roles that map to primitives). Primitive tokens hold the oklch() or HEX value: --color-blue-500: oklch(0.612 0.212 261.5). Semantic tokens reference primitives: --color-interactive: var(--color-blue-500). Components then use semantic tokens: .button { background: var(--color-interactive); }. Consequently, reassigning --color-interactive to a different primitive updates every component that uses the interactive role, without touching component styles. Building on this, the two-layer structure separates color decisions (semantic layer) from color definitions (primitive layer). This separation means a designer can swap the entire palette by editing only the primitive layer while every component that consumes semantic tokens reflects the change instantly.

Dark mode with custom properties

Dark mode switches the semantic token values, not the component styles. In :root, light mode defines --bg-base: oklch(0.97 0 0) (near-white) and --text-primary: oklch(0.20 0 0) (near-black). A [data-theme="dark"] selector or @media (prefers-color-scheme: dark) block redefines those same tokens: --bg-base: oklch(0.12 0 0) and --text-primary: oklch(0.90 0 0). Components that use var(--bg-base) and var(--text-primary) switch automatically without any component-level changes. Furthermore, oklch() makes light-to-dark token transitions predictable because the L channel directly controls perceived lightness across the entire palette2. Swapping L values between light and dark modes keeps hue and chroma consistent, so a brand blue reads as the same blue on both themes even though its lightness differs.

Component-scoped overrides and cascade layers

CSS custom properties follow the standard cascade: a custom property defined on a more specific element overrides the root definition for that element and its descendants. A sidebar component can define --bg-base: oklch(0.15 0 0) on its own scope, making all nested components use the darker background without modifying global tokens. @layer rules control priority between token systems3. A @layer base for primitives and a @layer theme for semantic tokens lets third-party component libraries participate without specificity conflicts. Yet custom property inheritance means a single override in the wrong scope can propagate unexpectedly; always scope overrides to the smallest necessary container.

White-labeling and multi-brand theming with custom properties

CSS custom properties make white-label theming possible without duplicating stylesheets. A single component library can serve multiple brands by swapping the custom property values at runtime. Define all colors as semantic tokens (--color-interactive, --color-surface, --color-text) and provide a brand-specific property file that assigns oklch() values to each token. Loading a different brand stylesheet rethemes the entire application without touching any component CSS. This pattern is the foundation of design systems that serve multiple products or tenants from a shared codebase.

Runtime theme switching with JavaScript

Switching themes at runtime requires changing custom property values on the :root element or a theme wrapper. The simplest approach sets a data-theme attribute on document.documentElement and uses attribute selectors in CSS: [data-theme="brand-b"] { --color-interactive: oklch(0.55 0.25 145); }. For smoother transitions, add transition: background-color 0.2s, color 0.2s to elements that reference color tokens. Custom properties that hold color values cannot be transitioned directly unless registered with @property, but transitioning the properties that use them (background-color, color) achieves the same visual effect.

The runtime switch also composes with persisted user preference, since the same data-theme attribute can be read on page load and applied before the first paint. That ordering avoids a flash of the wrong theme because the choice is set from storage before any component renders. Keeping the theme state in a single attribute rather than many scattered classes is what makes the switch cheap enough to run on every navigation without noticeable cost.

OKLCH custom properties and the @property registration

Registering a custom property with @property tells the browser its syntax, which enables transitions and animations on that property. Writing @property --color-hue { syntax: "<number>"; inherits: true; initial-value: 261; } registers --color-hue as a numeric value the browser can interpolate.4 You can then animate --color-hue from 261 to 30 to shift a button from blue to orange on hover, with the browser handling the interpolation in OKLCH space. Without @property registration, the browser treats the custom property as a string and cannot interpolate it.

Using @property with OKLCH channel decomposition

Decomposing an OKLCH color into separate --color-l, --color-c, and --color-h custom properties gives you independent animatable channels. A hover effect that increases lightness animates only --color-l, leaving --color-c and --color-h unchanged. Register each channel with the appropriate syntax: @property --color-l { syntax: "<number>"; inherits: true; initial-value: 0.612; }. This approach is more flexible than animating a single --color-primary property because it lets you control which perceptual dimension changes during the transition.

Scoped tokens and the cascade in component libraries

Component libraries that use custom properties for color can be scoped to a container element. A card component that defines --card-bg: var(--color-surface) and --card-text: var(--color-text-primary) on its own class inherits from whatever the parent scope sets for those tokens. Placing the card inside a sidebar that redefines --color-surface to a darker value automatically rethemes the card without any card-specific dark mode logic. This cascade-driven scoping is the primary advantage of custom properties over Sass variables, which are resolved at compile time and cannot respond to runtime context.

Avoiding token name collisions

Large projects with multiple component libraries risk custom property name collisions when two libraries define --color-primary with different values. The standard mitigation is namespacing: a library prefixes its tokens with its own identifier (--lib-button-color-primary, --lib-card-color-surface). This adds verbosity but prevents silent overrides. An alternative is CSS @scope, which limits the reach of custom property definitions to a specific subtree, though browser support for @scope is still maturing as of 2025.5

When to use this

Use this when setting up a CSS design token system that uses custom properties for color, particularly when implementing dark mode, component-scoped theming, or a two-layer (primitive/semantic) color architecture.

Notes

Custom properties are case-sensitive: --color-primary and --Color-Primary are different properties. Fallback values in var() apply when the property is undefined, not when it is invalid. OKLCH values in custom properties require modern browsers, so provide HEX fallbacks for older browser support.

Examples

Primitive + semantic token pattern

:root {
  --color-blue-500: oklch(0.612 0.212 261.5);
  --color-interactive: var(--color-blue-500);
}

Components use --color-interactive; only the semantic layer changes per theme

Dark mode via data-theme attribute

[data-theme="dark"] {
  --bg-base: oklch(0.12 0 0);
  --text-primary: oklch(0.90 0 0);
}

Toggle with document.documentElement.setAttribute("data-theme", "dark")

Component-scoped override

.sidebar {
  --bg-base: oklch(0.15 0 0);
}

All nested components inherit the sidebar's darker background

Verify with the Color Format Converter tool.

Primitive + semantic token pattern

:root {
  --color-blue-500: oklch(0.612 0.212 261.5);
  --color-interactive: var(--color-blue-500);
}

Components use --color-interactive; only the semantic layer changes per theme

Sources
  1. 1.

    CSS Working Group, "CSS Custom Properties for Cascading Variables Module Level 1," w3.org, June 2022. https://www.w3.org/TR/css-variables-1/

  2. 2.

    Mozilla, "oklch()," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/oklch

  3. 3.

    CSS Working Group, "CSS Cascading and Inheritance Level 6," w3.org, accessed June 2026. https://www.w3.org/TR/css-cascade-6/

  4. 4.

    Mozilla, "Registering custom properties in CSS," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Properties_and_values_API/Registering_properties

  5. 5.

    web.dev, "Web Platform December 2025," web.dev, December 2025. https://web.dev/blog/web-platform-12-2025

FAQ