# Recent colors

> Persist the recent swatches in the React color picker with recentColors and onRecentColorsChange, so the list survives a reload instead of resetting.

Source: https://chroma-panel.jscrate.dev/react/recipes/recent-colors
Last updated: 2026-09-18

The picker keeps a row of recently used colors in its footer, and by default
that row starts empty on every mount. Persist it when people come back to the
same picker often, such as a canvas editor or an admin theme screen, where
re-finding a color they used yesterday is the slow part. The list is a plain
array of strings, so where you keep it is up to you.

```tsx
"use client";

import { ColorInput } from "chroma-panel";
import "chroma-panel/style.css";
import { useEffect, useState } from "react";

const STORAGE_KEY = "chroma-panel:recent";

export function BrandColorInput() {
  const [recent, setRecent] = useState<string[]>([]);

  useEffect(() => {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    if (raw === null) return;
    try {
      const parsed: unknown = JSON.parse(raw);
      if (Array.isArray(parsed)) {
        setRecent(parsed.filter((c): c is string => typeof c === "string"));
      }
    } catch {
      window.localStorage.removeItem(STORAGE_KEY);
    }
  }, []);

  return (
    <ColorInput
      name="brand"
      defaultValue="#3366cc"
      recentColors={recent}
      onRecentColorsChange={(colors) => {
        setRecent(colors);
        window.localStorage.setItem(STORAGE_KEY, JSON.stringify(colors));
      }}
    />
  );
}
```

The read happens in an effect, not during render, so the server and the first
client render both produce an empty list and nothing mismatches. See
[server rendering](https://chroma-panel.jscrate.dev/react/handbook/server-rendering) for the general rule.

## What goes into the list

The panel adds a color at the same moment `onChangeComplete` fires: when a
change is committed, not while a drag is in progress. It adds it as a six-digit
hex string, so opacity is not part of the history even when `showAlpha` is on.

Before adding, the list is de-duplicated by color rather than by string, so
`#3366CC` and `#3366cc` are one entry, and then capped at ten. `onRecentColorsChange`
hands you the finished list every time, which is why the callback above can
store its argument as-is.

> **One swatch may be missing**
>
> The footer hides the recent swatch that matches the color currently selected,
> since clicking it would do nothing. The stored list is still complete; it is
> only the row that looks one short.

## Controlled or uncontrolled

`recentColors` and `defaultRecentColors` follow the same pattern as `value` and
`defaultValue`, described in
[controlled and uncontrolled](https://chroma-panel.jscrate.dev/react/handbook/controlled).

Pass `defaultRecentColors` to seed the list and let the panel manage it from
there. It is read once, when the panel mounts, so a list you read out of storage
in an effect arrives too late for it. That is what `recentColors` is for, and
why the example at the top uses it.

```tsx
<ColorInput defaultRecentColors={["#3366cc", "#cc3366"]} />
```

Pass `recentColors` and the panel stops keeping its own copy. It will call
`onRecentColorsChange` with what the new list should be and render whatever you
pass back, so forgetting to store the result leaves the row frozen.

## Adding a color yourself

If you have swatch buttons of your own outside the panel, `pushRecent` applies
the same rules the panel uses, so both paths produce one consistent history.

```tsx
import { pushRecent } from "chroma-panel";

setRecent((current) => pushRecent(current, "#cc3366"));
```

The signature is `pushRecent(list, hex, limit?)`, and `limit` defaults to ten.
It returns a new array and does not mutate the one you pass.

## Turning the row off

`showRecentColors={false}` removes it. Reach for that in a one-shot picker,
where there is no history worth keeping. The row itself, and the eyedropper
beside it, are described on the
[PanelFooter reference](https://chroma-panel.jscrate.dev/react/components/panel-footer).
