Skip to content

Add oklch color format support for animations#3637

Merged
mattgperry merged 1 commit intomainfrom
worktree-fix-issue-3058
Mar 16, 2026
Merged

Add oklch color format support for animations#3637
mattgperry merged 1 commit intomainfrom
worktree-fix-issue-3058

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Bug: oklch() colors were not recognized as animatable, causing animations between oklch and other formats (hex, rgb, hsl) to snap instantly with a console warning: 'oklch(...)' is not an animatable color
  • Cause: The color regex patterns and parser only matched #hex, rgb(a), and hsl(a) formats. oklch() strings failed the singleColorRegex test, so asRGBA() returned false and mixColor() fell back to mixImmediate
  • Fix: Extended the color regex to match oklch(), added an oklch parser and oklch-to-RGBA conversion (OKLCH → OKLAB → LMS → linear sRGB → sRGB), and wired it into the color mixing pipeline

Fixes #3058

Test plan

  • Added unit tests for mixColor with oklch colors (oklch-to-hex, hex-to-oklch, oklch with alpha)
  • All existing color mixing tests still pass (23 total)
  • Full yarn build passes (all 8 packages)
  • Full yarn test passes (769 tests across all packages)

🤖 Generated with Claude Code

@greptile-apps
Copy link

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR adds oklch() color format support to Motion's animation pipeline by extending the color regex patterns, adding an oklch parser, and implementing a mathematically correct OKLCH → RGBA conversion (via OKLAB → LMS → linear sRGB → sRGB). Standalone oklch() color animations now work correctly, fixing the reported issue (#3058).

However, there is one critical gap that needs to be addressed before merging:

  • complexRegex not updatedpackages/motion-dom/src/value/types/complex/index.ts contains a private, hardcoded tokenizer regex (complexRegex, line 44) that is separate from the exported colorRegex and singleColorRegex. It was not updated to include oklch. This means complex CSS values that embed an oklch color — such as box-shadow: oklch(0.65 0.18 260) 2px 2px 4px — will be incorrectly tokenized: the three numbers inside the oklch function will be extracted as independent number tokens rather than a single color, silently producing garbled animation output.

Additional concerns:

  • color/index.ts transform now accepts Color (which includes OKLCH) but has no guard for OKLCH objects, silently falling through to hsla.transform. While harmless in current code paths (since color.parse always converts oklch to RGBA), it is a maintenance risk.
  • The new unit tests only assert output format (/^rgba\(/) rather than specific expected values; a coefficient typo in the conversion matrix would go undetected.

Confidence Score: 2/5

  • Not safe to merge — the complexRegex gap causes silent breakage for oklch inside complex CSS values.
  • The primary goal of the PR (standalone oklch color animations) is correctly implemented with sound math. However, a parallel code path — complexRegex in complex/index.ts — was not updated, meaning oklch colors embedded in multi-part CSS values (box-shadow, gradients, text-shadow) will silently produce wrong animation output. This is a non-obvious, uncovered bug that the existing test suite doesn't catch because no test exercises the complex value path with oklch.
  • packages/motion-dom/src/value/types/complex/index.ts (complexRegex on line 44 must be updated), packages/motion-dom/src/value/types/color/index.ts (transform fallthrough for OKLCH objects)

Important Files Changed

Filename Overview
packages/motion-dom/src/value/types/complex/index.ts Not changed in this PR, but contains complexRegex — a hardcoded tokenizer regex that was NOT updated to include oklch. Complex CSS values with oklch colors (box-shadow, gradients, etc.) will be incorrectly tokenized, extracting the raw numbers as independent tokens instead of a single color.
packages/motion-dom/src/value/types/color/oklch-to-rgba.ts New file implementing OKLCH → OKLAB → LMS → linear sRGB → sRGB conversion. Coefficients match the published OKLAB reference. Negative out-of-gamut linear RGB values are safely handled by the clamp call. No issues found.
packages/motion-dom/src/value/types/color/index.ts Updated color.test() and color.parse() to handle oklch strings. color.parse() correctly converts oklch to RGBA immediately. However, color.transform() is not updated to handle OKLCH objects despite Color now including OKLCH, creating a silent fallthrough to hsla.transform for any OKLCH object passed directly.
packages/motion-dom/src/utils/mix/color.ts Correctly wires oklch into the color mixing pipeline — getColorType and asRGBA are both updated to handle oklch strings and convert them to RGBA before blending. Logic is sound.
packages/motion-dom/src/utils/mix/tests/mix-color.test.ts Three new tests added for oklch mixing, but they only verify output format (/^rgba\(/) rather than specific color values, meaning math regressions would not be caught. No test covers oklch inside a complex value (e.g., box-shadow).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Input color string] --> B{Is it a complex value?}
    B -- No --> C{getColorType}
    B -- Yes --> D[analyseComplexValue\nuses complexRegex]

    D --> E{complexRegex matches oklch?}
    E -- No - BUG --> F["oklch numbers extracted\nas individual number tokens\n❌ Wrong animation output"]
    E -- Yes - after fix --> G[color.parse called\nconverts oklch → RGBA\n✅]

    C --> H{type === hex / rgba / hsla / oklch}
    H -- oklch --> I[oklch.parse\nlightness, chroma, hue, alpha]
    I --> J[oklchToRgba\nOKLCH → OKLAB → LMS → sRGB]
    J --> K[RGBA object]

    H -- hex/rgba/hsla --> L[Existing parsers]
    L --> K

    K --> M[mixLinearColor blends\nred, green, blue, alpha]
    M --> N[rgba.transform\noutput: rgba string ✅]
Loading

Comments Outside Diff (2)

  1. packages/motion-dom/src/value/types/complex/index.ts, line 43-44 (link)

    complexRegex not updated to include oklch

    The hardcoded complexRegex at line 43-44 is used by analyseComplexValue to tokenize complex CSS values. It was not updated to support oklch, meaning complex CSS values that contain an oklch color — e.g., box-shadow: oklch(0.65 0.18 260) 2px 2px 4px or background: linear-gradient(oklch(0.65 0.18 260), #fff) — will be incorrectly tokenized.

    Instead of extracting oklch(0.65 0.18 260) as a single color token and passing it to color.parse(), the regex will fall through to the digit-matching alternative (-?(?:\d+(?:\.\d+)?|\.\d+)) and extract 0.65, 0.18, and 260 as three separate number tokens. The animation will then interpolate those raw numbers rather than blending the color — producing garbled output with no warning.

    Contrast with colorRegex (in color-regex.ts) and singleColorRegex (in single-color-regex.ts), which were correctly updated. The complexRegex is a private, hardcoded copy that must also be kept in sync.

  2. packages/motion-dom/src/value/types/color/index.ts, line 21-27 (link)

    transform silently falls through to hsla.transform for OKLCH objects

    Color now includes OKLCH, so the transform function's type signature accepts oklch objects. However, the function only guards for red (RGBA) and falls back to hsla.transform for everything else — which would silently produce an incorrectly-formatted hsla(...) string for any OKLCH object that reaches this point.

    In the current code paths this is harmless because color.parse() always converts oklch to RGBA before storing. But the signature gap is a maintenance trap: future callers passing an OKLCH object directly would get wrong output with no error.

    Consider adding an explicit guard:

    transform: (v: Color | string) => {
        return typeof v === "string"
            ? v
            : v.hasOwnProperty("red")
            ? rgba.transform(v as RGBA)
            : v.hasOwnProperty("chroma")
            ? rgba.transform(oklchToRgba(v as OKLCH))
            : hsla.transform(v as HSLA)
    },

Last reviewed commit: 9f17746

Comment on lines +132 to +146
test("mixColor oklch to hex", () => {
const mix = mixColor("oklch(0.65 0.18 260)", "#ffffff")
expect(mix(0.5)).toMatch(/^rgba\(/)
})

test("mixColor hex to oklch", () => {
const mix = mixColor("#ffffff", "oklch(0.65 0.18 260)")
expect(mix(0.5)).toMatch(/^rgba\(/)
})

test("mixColor oklch with alpha", () => {
const mix = mixColor("oklch(0.65 0.18 260 / 0.5)", "#ffffff")
expect(mix(0)).toMatch(/^rgba\(/)
expect(mix(0)).toContain("0.5)")
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests verify format only, not correctness of color math

All three new oklch tests only assert that the output starts with rgba(, which would pass even if the conversion matrix had a typo. Consider adding at least one test that checks a known, expected output value to guard against regression in the math. For example, oklch(1 0 0) (lightness = 1, chroma = 0, any hue) should equal pure white rgba(255, 255, 255, 1), and oklch(0 0 0) should equal pure black rgba(0, 0, 0, 1):

test("mixColor oklch achromatic white", () => {
    // Pure white in OKLCH: L=1, C=0 → rgb(255,255,255)
    expect(mixColor("oklch(1 0 0)", "#ffffff")(0)).toBe("rgba(255, 255, 255, 1)")
})

@mattgperry mattgperry force-pushed the worktree-fix-issue-3058 branch 4 times, most recently from d54b331 to e33ea64 Compare March 16, 2026 12:42
When animating color properties with modern CSS color formats like oklch()
that the JS animation path can't parse, force WAAPI which handles native
browser color interpolation. Falls back to JS animation if WAAPI throws.

Fixes #3058

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mattgperry mattgperry force-pushed the worktree-fix-issue-3058 branch from e33ea64 to 5500225 Compare March 16, 2026 12:43
@mattgperry mattgperry merged commit fec1b53 into main Mar 16, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] animation fails when transitioning between oklch and hex color formats

1 participant