Changelog

Notable changes to the color picker, newest first.

  1. v1.2.0

    July 26, 2026Cleaner color samples, and gradient stops you can track by id

    Improvements

    • Color samples no longer have a ring around them. Swatches, gradient presets and the preview each drew a thin grey border, which looked like a halo — obvious around a dark color on a light page, and invisible around a dark color on a dark page, so it showed up in the one place it wasn't wanted. The edge is now drawn faintly inside the sample instead: it fades away on strong colors and only shows on pale ones, so a near-white swatch still has a visible edge.
    • Gradient stops can now carry an id. If you control the gradient from your own state, the picker used to work out which stop was which by where it sat on the bar. That falls apart when two stops sit in the same spot — reorder them and the colors could end up on the wrong stop. Give each stop an id and the picker follows it exactly, so the selected stop, its color, and its chosen format all stay together.
    • Stop ids are optional and nothing changes if you skip them. Leave them out and the picker behaves exactly as it did before, and never adds an id to what it hands back.
    • All the picker's types are now listed in the docs in one place, under API: Types — several were mentioned in the docs without ever being written down.

    Bug fixes

    • A gradient whose stops weren't listed in order came out as a single flat color instead of a gradient, with nothing reported. The picker now puts them in order first.
    • Adding a stop to a gradient that had no stops crashed instead of adding one.
    • The gradient CSS box could undo a change made elsewhere in your app: click into the box, have something else update the gradient, click away without typing, and the old text was put back. It now leaves the newer value alone.
    • Deleting a stop with the keyboard on the gradient bar sent focus back to the top of the page, so you couldn't carry on with the keyboard. It now moves to the next stop along. Trying to delete the last remaining stop did nothing at all with no explanation; it now says why.
    • The Base UI hue, alpha and lightness sliders ignored a ref passed to them, so it was always empty.
    How gradient state is shaped, including stop ids →
    Adopt gradient stop ids

    Only needed if you drive the gradient picker from your own state AND can end up with two stops in the same spot (or reorder stops outside the picker). If that isn't you, skip it — the prompt checks first and stops.

    # Migration: opt into gradient stop ids (amplo-color-picker 1.2.0)
    
    ## Context
    `GradientStop` gained an optional `id?: string`. It is opt-in — if the app
    does not use it, nothing needs to change and this migration can be skipped.
    
    Without ids, a **controlled** `<GradientPicker.Root value={...}>` reconciles an
    incoming gradient against its internal state by stop position and array index.
    That cannot distinguish two stops sharing the same position, so reordering them
    externally moves the colors between stops while the selected stop and per-stop
    color format stay pointing at the old ones.
    
    ## When to apply
    Apply ONLY if the app both:
    1. renders `<GradientPicker.Root>` (or `<FillPicker.Pane mode="gradient">`) in
       controlled mode — i.e. passes `value`, not just `defaultValue`; AND
    2. can produce a gradient where two stops share the same `position`, or
       reorders/rebuilds the `stops` array outside the picker (drag-to-reorder,
       collaborative edits, undo/redo, server sync).
    
    If neither holds, make NO changes and report that the app is unaffected.
    
    ## Task
    1. Find every controlled use of the gradient picker and trace where the
       `Gradient` value is created and stored.
    2. Give each stop a stable `id` at the point stops are first created. Reuse an
       identifier that already exists in the domain model (a database row id, for
       example) rather than inventing a parallel one. Do NOT derive the id from the
       stop's position or array index — that reintroduces the exact ambiguity this
       fixes.
    3. Ids must be unique within a single gradient. Duplicates are ignored by the
       picker and fall back to a generated id.
    4. It is all-or-nothing per gradient: a half-tagged stop array is treated as
       untagged. Make sure every stop of a gradient gets one.
    5. Ids round-trip — `onValueChange` echoes them back, including on stops added
       inside the picker — so `onValueChange={(g) => setGradient(g)}` is enough to
       persist them. Do not strip `id` when storing, and do not regenerate ids on
       every render.
    6. If gradients are serialized (localStorage, a database, an API), confirm the
       stored shape tolerates the extra `id` field. Add it to any schema or
       validator that rejects unknown keys.
    
    ## Verify
    - Build and typecheck cleanly.
    - With two stops at the same position, select one, change its color, then
      reorder the stops array externally: the selection and the edited color must
      stay on the same stop.
    - An app that opts out must see byte-identical behavior, and no `id` key in
      anything `onValueChange` emits.
    
  2. v1.1.0

    July 6, 2026Base UI variant — now the default
    • New Base UI variant of the picker, built on Base UI primitives (Slider, Select, NumberField, RadioGroup). It shares the exact same OKLCH engine and compound API as the original, so behavior and fixes stay in lockstep.
    • Base UI is now the default variant. The Radix-backed original moved to /docs/radix — switch between them anytime with the toggle at the top of the docs and playground.
    • Accessibility fix: the Base UI sliders now expose their accessible name on the underlying range input (aria-label moved from the wrapper to the thumb).
    • Docs and playground gained a Base UI / Radix switcher; install commands and copy-paste code follow the selected variant.
  3. v1.0.0

    May 4, 2026Initial release
    • OKLCH-native, Display-P3-aware color / fill picker distributed as a shadcn registry.
    • Compose-only parts: Area, Hue, Lightness, Alpha, FormatSwitcher, ChannelInput, Swatches, GamutBadge, ContrastReadout, Preview, EyeDropper, and CssInput.
    • Lossless format toggles (hex / rgb / hsl / hsb / oklch / oklab / display-p3), WCAG + APCA contrast metrics, gamut detection with soft-proofing, and full keyboard accessibility.