@pbroom/color-kit/reactColor

Color

Provides shared color state to every component inside it.

componentimport { Color } from '@pbroom/color-kit/react';color.tsx:54
function Color(props: ColorProps): Element;

Accepts the same options as useColor (uncontrolled defaultColor, or controlled state + onChange). Descendants subscribe to the slices of state they read, so an uncontrolled provider does not rerender on color changes and a drag only rerenders the components that depend on it. Read it with useColorContext or useColorStoreSelector.

Props

From ColorProps; other attributes pass through to the element.

childrenReactNode
Color components that share this provider's state.
defaultColor?string | Colordefault { l: 0.6, c: 0.2, h: 250, alpha: 1 }
Initial requested color in uncontrolled mode: any CSS color string parse accepts, or an OKLCH Color object. Ignored when state is set.
state?ColorState
Controlled full state. When set, the hook renders this state and reports updates through onChange instead of storing them.
onChange?(event: ColorUpdateEvent) => void
Called when a setter produces a new state. Fires synchronously inside the setter call (i.e. in your event handler, before React re-renders) in both controlled and uncontrolled modes, exactly once per effective update and never for no-op updates. Several setter calls in one tick compose: each event.next builds on the previous one, even in controlled mode before the new state prop arrives. Once React re-renders, later calls start from the state you committed, so an update you ignore is not carried into the next one.
defaultGamut?GamutTargetdefault 'display-p3'
Initial active display gamut in uncontrolled mode.
defaultView?ViewModeldefault 'oklch'
Initial active view model in uncontrolled mode.

Example

TSX
import { Color, useColorContext, useColorPlaneRenderer } from '@pbroom/color-kit/react';

function Plane() {
  const { requested } = useColorContext();
  const { ref, canvasKey } = useColorPlaneRenderer({ color: requested });
  return <canvas ref={ref} key={canvasKey} style={{ width: 240, height: 160 }} />;
}

function Hue() {
  const { requested, setChannel } = useColorContext();
  return (
    <input
      type="range"
      aria-label="Hue"
      min={0}
      max={360}
      value={requested.h}
      onChange={(event) => setChannel('h', Number(event.currentTarget.value))}
    />
  );
}

export const Picker = () => (
  <Color defaultColor="#ff6600">
    <Plane />
    <Hue />
  </Color>
);

Edge cases

  • The provider does not re-render. <Color> passes a stable store through context and never re-renders its children on a color change; components re-render when the store slice they subscribe to changes. Reading the context with useContext(ColorContext) gives that stable store and the setters, not live fields; use useColorContext for a live snapshot or useColorStoreSelector for one field.
  • Controlled updates compose. With state and onChange, onChange fires synchronously inside each setter call, once per effective update and never for a no-op. Several setter calls in one event build on each other even before your new state arrives. After React re-renders, the next update starts from the state you passed, so an update you chose to drop is not carried forward.
  • defaultColor is read once. Changing it after mount has no effect, as with any uncontrolled input; pass state to drive the color from outside, or remount with a key.
  • Defaults. Without defaultColor the color is oklch(0.6 0.2 250); the active gamut defaults to 'display-p3' and the view to 'oklch'.

See also