# Custom palette and chrome colours

There are two sets of colours and they are set in two places. The video has a palette: it belongs to the frame, the viewer can change it, and it is baked into the exported file. The chrome around the video (the player's controls, the settings, the scan panel, the gallery) reads ten CSS custom properties that you set once in your stylesheet. This guide covers both.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/guides/palette-and-chrome/
>
> One accent in, a whole palette out.

## A palette from one colour

Set `palette` to `custom` and `accent` to your colour as `#rrggbb`. `mode` still says dark or light. Everything else is worked out from the accent, so the result always fits itself.

`own-colour.tsx`

```tsx
import { type CSSProperties, useMemo, useState } from "react";

import {
  CUSTOM_PALETTE,
  randomAccent,
  resolvePalette,
  type SceneInstance,
  SceneThumb,
  type WrappedConfig,
} from "@react-wrapped/core";

const COLUMNS: CSSProperties = { display: "flex", flexWrap: "wrap", gap: 24, alignItems: "flex-start" };
const SWATCHES: CSSProperties = { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 14 };
const SWATCH: CSSProperties = {
  width: 34,
  height: 34,
  borderRadius: 9,
  boxShadow: "inset 0 0 0 1px rgb(0 0 0 / 0.12)",
};

export function OwnColour({ scene, config }: { scene: SceneInstance; config: WrappedConfig }) {
  const [accent, setAccent] = useState("#ff5c8a");
  // One colour in, a whole palette out. Dark or light still comes from the config's `mode`.
  const own = useMemo((): WrappedConfig => ({ ...config, palette: CUSTOM_PALETTE, accent }), [config, accent]);
  const palette = resolvePalette(own);
  return (
    <div style={COLUMNS}>
      <SceneThumb scene={scene} config={own} width={280} loop />
      <div>
        <div role="group" aria-label="Accent colour">
          <input
            type="color"
            aria-label="Accent colour"
            value={accent}
            onChange={(event) => setAccent(event.target.value)}
          />
          <button type="button" onClick={() => setAccent(randomAccent())}>
            Surprise me
          </button>
        </div>
        {/* The five levels the accent was stretched into, then the four rarity colours cut from it. */}
        <div style={SWATCHES}>
          {[...palette.levels, ...Object.values(palette.rarity)].map((colour, index) => (
            // oxlint-disable-next-line react/no-array-index-key -- nine fixed swatches, and two may share a colour
            <i key={index} title={colour} style={{ ...SWATCH, background: colour }} />
          ))}
        </div>
        <output>{`palette: "custom", accent: "${accent}"`}</output>
      </div>
    </div>
  );
}
```

- Level 0 is a quiet tint of the background. Levels 1 to 3 are the accent mixed into the background at 30, 55 and 80 percent, and level 4 is the accent itself.
- Rare and epic are the accent's neighbours on the colour wheel, 48 degrees to each side. Legendary is gold, or a light tint of the accent when the accent is already gold.
- On the light background the rarity colours are deepened so they still read.
- `randomAccent()` gives a bright, saturated colour at a random hue that reads on both backgrounds.

## resolvePalette

`resolvePalette(config)` turns the three config keys into a `WrappedPalette`: the colours as an object. The stage writes it out as CSS variables, and the scan, the gallery and the card renderers take it as a prop, which is how they wear the theme of the video.

| Key | Type | What it is |
| --- | --- | --- |
| `bg` | `string` | The frame's background. `--wr-bg` on the stage. |
| `card` | `string` | A panel standing on the background. `--wr-card`. |
| `fg` | `string` | Text. `--wr-fg`. |
| `muted` | `string` | Quieter text. `--wr-muted`. |
| `levels` | `readonly [string, string, string, string, string]` | Five steps, quietest to loudest. `--wr-l0` to `--wr-l4`. The last one is the accent. |
| `rarity` | `Record<Rarity, string>` | A colour per rarity. `--wr-common`, `--wr-rare`, `--wr-epic` and `--wr-legendary`. |

```tsx
import { DEFAULT_WRAPPED_CONFIG, paletteStyle, resolvePalette } from "@react-wrapped/core";

const palette = resolvePalette({ ...DEFAULT_WRAPPED_CONFIG, palette: "custom", accent: "#ff5c8a" });

// The same colours as CSS variables, for an element of your own that should match the frame.
export const frameColours = paletteStyle(palette);
```

The built-in palettes are in `WRAPPED_PALETTES`, each with a `label`. `themePalette(id, mode)` gives one of them in dark or light, for a picker's swatch, and `isPresetPalette(id)` tells a built-in id from `custom`. The variables themselves are listed under [theming](https://bettertyped.github.io/react-wrapped/docs/theming/).

## Chrome tokens

Everything outside the frame is painted with these. The library sets its own values on the page's root with no weight at all, so any rule of yours wins: on `:root`, on `body`, or on a wrapper around one part.

| Token | What it paints |
| --- | --- |
| `--wr-ui-bg` | The background of a full screen player, and text standing on a solid button. |
| `--wr-ui-fg` | Text, and the solid buttons. |
| `--wr-ui-card` | Panels and cards: the settings' swatches, the timeline's hover card, dialogs. |
| `--wr-ui-muted` | Quiet fills: chips at rest, the empty part of the timeline. |
| `--wr-ui-muted-fg` | Quiet text: the clock, hints, descriptions. |
| `--wr-ui-primary` | The one accent: the played part of the timeline, the highlighted words in headings. |
| `--wr-ui-primary-fg` | Text on the accent. |
| `--wr-ui-border` | Hairlines and outlines. |
| `--wr-ui-ring` | The focus ring. |
| `--wr-ui-radius` | The base corner radius. |
| `--wr-font` | The chrome's font. Left out, the chrome uses the font of the page around it. |

```css
:root {
  --wr-ui-primary: oklch(0.62 0.2 285);
  --wr-ui-card: oklch(0.97 0.02 285);
  --wr-ui-border: oklch(0.86 0.05 285);
  --wr-ui-radius: 6px;
  --wr-font: "Your Font", sans-serif;
}

/* Your rule on :root also beats the library's dark values, so say what dark should be. */
.dark {
  --wr-ui-primary: oklch(0.74 0.16 285);
  --wr-ui-card: oklch(0.25 0.03 285);
  --wr-ui-border: oklch(0.34 0.05 285);
}
```

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/guides/palette-and-chrome/
>
> The same parts with and without a set of tokens on the wrapper.

`own-chrome.tsx`

```tsx
import { type CSSProperties, useState } from "react";

import type { SceneInstance, WrappedConfig, WrappedSubject } from "@react-wrapped/core";
import { WrappedConfigurator, WrappedPlayer } from "@react-wrapped/player";

// Set on any ancestor of the library's parts. Each one falls back to the library's own value when left out.
const BRAND = {
  "--wr-ui-bg": "oklch(0.985 0.012 285)",
  "--wr-ui-primary": "oklch(0.62 0.2 285)",
  "--wr-ui-card": "oklch(0.97 0.02 285)",
  "--wr-ui-muted": "oklch(0.93 0.03 285)",
  "--wr-ui-border": "oklch(0.86 0.05 285)",
  "--wr-ui-fg": "oklch(0.25 0.08 285)",
  "--wr-ui-muted-fg": "oklch(0.48 0.08 285)",
  "--wr-font": "Georgia, serif",
} as CSSProperties;

// Your page around the parts can read the same tokens, so the two always match.
const COLUMNS: CSSProperties = {
  display: "flex",
  flexWrap: "wrap",
  gap: 24,
  alignItems: "flex-start",
  marginTop: 14,
  padding: 16,
  borderRadius: 18,
  background: "var(--wr-ui-bg)",
  color: "var(--wr-ui-fg)",
};
const ROOM: CSSProperties = { display: "grid", height: 520, flex: "1 1 260px", minWidth: 0 };
const PANEL: CSSProperties = { flex: "1 1 300px", minWidth: 0 };

export function OwnChrome({
  subject,
  seed,
  scenes,
  config,
  onConfigChange,
}: {
  subject: WrappedSubject;
  seed: string;
  scenes: readonly SceneInstance[];
  config: WrappedConfig;
  onConfigChange: (next: WrappedConfig) => void;
}) {
  const [branded, setBranded] = useState(true);
  return (
    <div>
      <label>
        <input type="checkbox" checked={branded} onChange={(event) => setBranded(event.target.checked)} />
        Use our tokens
      </label>
      {/* The video keeps its own palette: the tokens only reach the chrome around it. */}
      <div style={branded ? { ...COLUMNS, ...BRAND } : COLUMNS}>
        <div style={ROOM}>
          <WrappedPlayer subject={subject} seed={seed} scenes={scenes} config={config} />
        </div>
        <div style={PANEL}>
          <WrappedConfigurator config={config} onChange={onConfigChange} />
        </div>
      </div>
    </div>
  );
}
```

## Past colour: slots

The tokens change colours, the radius and the font. To change the shape or the size of one part, such as the play button or a settings chip, give that part a class of your own. Every part of the chrome is a slot with a name: it carries `data-wr-slot="player-play"` and takes a class through `classNames`. The [styling guide](https://bettertyped.github.io/react-wrapped/docs/guides/styling/) lists every slot, and says how to make Tailwind utilities win over the library's classes.

## Dark mode

The chrome is dark wherever an ancestor has the class `dark`, the way most apps switch themes. There is nothing to pass and nothing to call: toggle the class on `html` and the library follows. A player in full screen is always dark, whatever the page is.

## What the tokens do not touch

- The frame. Its colours are the palette's, so an exported video looks the same on any site.
- The scan panel and the gallery tiles. They sit on the palette's background on purpose, so the year looks the way it does in the video.
- The frame's font. The stage, the share card and the insight cards are set in a font named Inter, with figures of equal width so a counting number does not shiver. See [installation](https://bettertyped.github.io/react-wrapped/docs/installation/) for loading it.

## Related

- [Styling](https://bettertyped.github.io/react-wrapped/docs/guides/styling/)
- [Theming](https://bettertyped.github.io/react-wrapped/docs/theming/)
- [WrappedConfigurator](https://bettertyped.github.io/react-wrapped/docs/player/configurator/)
- [Installation](https://bettertyped.github.io/react-wrapped/docs/installation/)
- [The config in the URL](https://bettertyped.github.io/react-wrapped/docs/guides/config-in-the-url/)
- [Rarity and drops](https://bettertyped.github.io/react-wrapped/docs/scanner/drops/)

Source: https://bettertyped.github.io/react-wrapped/docs/guides/palette-and-chrome/
