# Styling

Everything around the video can be restyled, at three levels. Tokens recolour all of it at once. Slots put a class of yours on one part: the play button, a timeline segment, a settings chip. And one line of CSS makes your Tailwind utilities win over the library's own classes. This guide goes through the three, cheapest first, and lists every slot there is.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/guides/styling/
>
> The same player and settings with and without a class per slot. Square buttons, outlined chips.

`own-style.tsx`

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

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

// Plain CSS classes of your own. This stylesheet comes after the library's, so on the same element a class here
// wins over the library's class. They read the chrome tokens, so they follow light and dark like everything else.
const CSS = `
.orbit-frame { border-radius: 4px; }
.orbit-button {
  border-radius: 6px;
  background: var(--wr-ui-primary);
  color: var(--wr-ui-primary-fg);
  box-shadow: 0 3px 0 color-mix(in oklch, var(--wr-ui-primary) 62%, black);
}
.orbit-clock { font-family: ui-monospace, monospace; color: var(--wr-ui-fg); }
.orbit-bar { border-radius: 2px; background: var(--wr-ui-border); }
.orbit-label { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--wr-ui-muted-fg); }
.orbit-chip {
  border-radius: 6px;
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--wr-ui-border);
}
/* The library styles a picked chip with the state in its selector, which outweighs a bare class. Name the state. */
.orbit-chip[aria-checked="true"] {
  background: var(--wr-ui-primary);
  color: var(--wr-ui-primary-fg);
  box-shadow: none;
}
.orbit-swatch { border-radius: 6px; }
`;

// A class per slot. The player takes its own slots and the timeline's.
const PLAYER: SlotClassNames<WrappedPlayerSlot | TimelineSlot> = {
  "player-frame": "orbit-frame",
  "player-play": "orbit-button",
  "player-restart": "orbit-button",
  "player-fullscreen": "orbit-button",
  "player-clock": "orbit-clock",
  "timeline-segment-bar": "orbit-bar",
};

const SETTINGS: SlotClassNames<ConfiguratorSlot> = {
  "settings-label": "orbit-label",
  "settings-chip": "orbit-chip",
  "settings-swatch": "orbit-swatch",
};

// Side by side where there is room, stacked on a phone.
const COLUMNS: CSSProperties = { display: "flex", flexWrap: "wrap", gap: 24, alignItems: "flex-start", marginTop: 14 };
const ROOM: CSSProperties = { display: "grid", height: 520, flex: "1 1 260px", minWidth: 0 };
const PANEL: CSSProperties = { flex: "1 1 300px", minWidth: 0 };

export function OwnStyle({
  subject,
  seed,
  scenes,
  config,
  onConfigChange,
}: {
  subject: WrappedSubject;
  seed: string;
  scenes: readonly SceneInstance[];
  config: WrappedConfig;
  onConfigChange: (next: WrappedConfig) => void;
}) {
  const [styled, setStyled] = useState(true);
  return (
    <div>
      <style>{CSS}</style>
      <label>
        <input type="checkbox" checked={styled} onChange={(event) => setStyled(event.target.checked)} />
        Use our classes
      </label>
      <div style={COLUMNS}>
        <div style={ROOM}>
          <WrappedPlayer
            subject={subject}
            seed={seed}
            scenes={scenes}
            config={config}
            classNames={styled ? PLAYER : undefined}
          />
        </div>
        <div style={PANEL}>
          <WrappedConfigurator config={config} onChange={onConfigChange} classNames={styled ? SETTINGS : undefined} />
        </div>
      </div>
    </div>
  );
}
```

## Tokens

Start here. Ten custom properties named `--wr-ui-*` hold every colour and the corner radius of the chrome, and `--wr-font` holds its font. Set them on any element above the library's parts and everything under it follows, in light and dark. No component has to be touched. They are listed in [custom palette and chrome colours](https://bettertyped.github.io/react-wrapped/docs/guides/palette-and-chrome/).

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

## Slots

Tokens change a colour everywhere. To change the shape, the size or the type of one part, reach that part. Every element of the chrome you might want to restyle is a slot: it has a name, it carries that name as `data-wr-slot="player-play"`, and it takes a class for it. A class reaches a slot in three ways.

### On one component

Every chrome component takes `classNames`: an object with a class per slot. `className` is still there and is the root's class. A component that renders another takes that one's slots too and hands them on, so the player takes the timeline's, and `WrappedView` takes all of them in one object.

```tsx
<WrappedPlayer
  subject={subject}
  seed={seed}
  scenes={scenes}
  config={config}
  className="my-player"
  classNames={{
    "player-play": "my-play-button",
    "player-frame": "my-frame",
    "timeline-segment-bar": "my-bar",
  }}
/>
```

### For the whole page

[`WrappedProvider`](https://bettertyped.github.io/react-wrapped/docs/guides/labels/) takes the same object and applies it to everything under it, so a design is written once. Keep the object the same between renders: a constant outside the component, or `useMemo`.

```tsx
import { type SlotClassNames, WrappedProvider, type WrappedSlot } from "@react-wrapped/kit";

// Typed against every slot there is, so a misspelt name does not compile.
const CLASS_NAMES: SlotClassNames<WrappedSlot> = {
  "player-play": "my-play-button",
  "settings-chip": "my-chip",
  "scan-start": "my-play-button",
  "gallery-tile": "my-tile",
};

export function Page({ children }: { children: React.ReactNode }) {
  return <WrappedProvider classNames={CLASS_NAMES}>{children}</WrappedProvider>;
}
```

### From a stylesheet

The attribute is there whether you pass a class or not, so a stylesheet can target a part with no React involved at all.

```css
[data-wr-slot="player-play"] {
  border-radius: 6px;
}

/* A picked chip, and only inside your own wrapper. */
.my-settings [data-wr-slot="settings-chip"][aria-checked="true"] {
  background: rebeccapurple;
}
```

### How the classes add up

- **The order on an element** is the library's own classes, then the provider's class for that slot, then the component's `classNames`. Nothing of the library's is taken away: your class is added after it.
- **Providers nest** the way they do for labels. A slot named by an inner provider takes the inner class in place of the outer one, and every other slot keeps what the outer provider gave it.
- **With no `classNames` anywhere** every part renders exactly the classes it did before. The only addition is the `data-wr-slot` attribute.
- **A state needs the state in your selector.** The library styles hover, focus and a picked choice with selectors that have the state in them, and those outweigh a bare class. To restyle a picked chip write `.my-chip[aria-checked="true"]`, or with Tailwind `aria-checked:bg-violet-600`.
- **The scan and the gallery wear the video's palette.** Their backgrounds and colours are inline styles, so the year looks the way it does in the video. A class changes their shape and type freely, and their colours only with `!important`. The tables below say which slots those are.
- **Two things are drawn outside their component's box.** The scan's flying card is drawn at the end of `body`, and the scene editor is a dialog in the top layer. `classNames` and the provider reach both. A stylesheet selector that starts from your own wrapper does not.
- **The video itself has no slots.** The frame's look is the palette and the scene, so an exported file is the same on any site. Restyle what is around it, and give the frame its corners through `"player-frame"`.

## With Tailwind

This needs one line of setup, and it is worth knowing why. The library's stylesheet is plain CSS, outside any cascade layer. Tailwind v4 puts its utilities in `@layer utilities`, and a rule in a layer loses to a rule outside one, whatever their order or weight. So out of the box a Tailwind class like `bg-red-500` passed to a slot does **not** win over the library's own class for the background.

The fix is to import the library's stylesheet into a layer that comes before utilities. In your app's CSS, after Tailwind:

`app.css`

```css
@import "tailwindcss";
@import "@react-wrapped/kit/styles.css" layer(components);
```

From then on any utility you pass through `className` or `classNames` wins, states included:

```tsx
<WrappedPlayer
  {...video}
  classNames={{
    "player-play": "rounded-md bg-violet-600 text-white",
    "player-frame": "rounded-md shadow-none ring-2 ring-violet-600",
  }}
/>
```

- **Load the stylesheet only this way.** Take the `import "@react-wrapped/kit/styles.css"` out of your JavaScript. A second copy outside the layer would still be on the page, and it would still win.
- **The same line works for the single packages**: import `@react-wrapped/core/styles.css` and the others into the same layer, core first.
- **Set the chrome tokens outside a layer**, or in a layer after `components`. The library's own values are now in `components`, so a `--wr-ui-*` set inside `@layer base` would lose to them.
- **Your unlayered rules now outrank the library too.** That is the point of the move, and it includes element rules: a bare `button { ... }` outside any layer reaches the library's buttons. Tailwind's own reset is in `base`, before `components`, so it does not.
- **Colours set inline** on the scan and the gallery still need the important modifier: `bg-violet-600!`.

## Without Tailwind

With plain CSS or CSS modules there is nothing to set up. Load the library's stylesheet before your own. A class of yours and a class of the library's then weigh the same, and yours wins by coming later. That is all the preview at the top of this page does.

```tsx
import "@react-wrapped/kit/styles.css";
// After the library's, so a class from here wins over the library's class on the same element.
import "./app.css";
```

When you cannot control the order, as with some CSS module setups, write the slot into the selector. An attribute and a class together outweigh the library's single class, so the rule wins wherever it is loaded.

```css
.playButton[data-wr-slot="player-play"] {
  border-radius: 6px;
  background: rebeccapurple;
}
```

## Every slot

Each component exports its slot names twice: as a type (`WrappedPlayerSlot`) for your own objects, and as a list (`WRAPPED_PLAYER_SLOTS`). `WrappedSlot` from `@react-wrapped/kit` is all of them together.

### WrappedPlayer

`WrappedPlayerSlot`. [`WrappedPlayerLoading`](https://bettertyped.github.io/react-wrapped/docs/player/loading/) has the same slots, so the frame and the controls look the same before the year arrives as after.

| Slot | What it is |
| --- | --- |
| `player` | The root: the video and the controls under it. `className` lands here too. |
| `player-room` | The space the frame is fitted into. |
| `player-frame` | The frame around the video: its corners, its shadow, its outline. |
| `player-transport` | The row of controls under the video. |
| `player-play` | The play and pause button. |
| `player-restart` | The button that plays from the start. |
| `player-clock` | Where the playhead is and how long the video runs. |
| `player-fullscreen` | The full screen button. |

### Timeline

`TimelineSlot`. The [player](https://bettertyped.github.io/react-wrapped/docs/player/wrapped-player/) takes these as well. The preview and everything in it is on the page only while a segment is hovered, and the controls only for a creator.

| Slot | What it is |
| --- | --- |
| `timeline` | The root: the row of segments and the preview that opens over it. |
| `timeline-track` | The row of segments, the part that is clicked and dragged. |
| `timeline-segment` | One scene's share of the row. |
| `timeline-segment-bar` | The bar drawn inside a segment. |
| `timeline-segment-fill` | The played part of a bar. |
| `timeline-popover` | What holds the preview over its segment. Its `z-index` is set here. |
| `timeline-preview` | The preview card. |
| `timeline-preview-thumb` | The box around the still of the scene. |
| `timeline-preview-title` | The scene's name. |
| `timeline-preview-length` | How long the scene plays, or that it is off. |
| `timeline-preview-rarity` | How rare a found scene is. Its colour is the palette's, set inline. |
| `timeline-controls` | The creator's row of controls in the card. |
| `timeline-move-earlier` | The arrow that moves the scene one place earlier. |
| `timeline-move-later` | The arrow that moves it one place later. |
| `timeline-edit` | The pencil that opens the scene editor. |
| `timeline-toggle` | The switch that takes the scene out of the video. |
| `timeline-toggle-thumb` | The dot that slides in that switch. |

### WrappedConfigurator

`ConfiguratorSlot`, on the [settings panel](https://bettertyped.github.io/react-wrapped/docs/player/configurator/).

| Slot | What it is |
| --- | --- |
| `settings` | The root. `className` lands here too. |
| `settings-field` | One setting: its name and what there is to pick. |
| `settings-label` | A setting's name. |
| `settings-palette` | The grid of palettes. |
| `settings-swatch` | One palette in the grid. It is a radio: the picked one has `aria-checked`. |
| `settings-swatch-colors` | The palette's five colours on its swatch. |
| `settings-swatch-color` | One of the five. |
| `settings-swatch-name` | The palette's name. |
| `settings-accent` | The row for a colour of your own, shown while the custom palette is picked. |
| `settings-accent-input` | The colour input in that row. |
| `settings-accent-random` | The button that picks a random colour. |
| `settings-chips` | A row of choices: dark or light, format, cell shape, speed. |
| `settings-chip` | One choice. It is a radio: the picked one has `aria-checked`. |
| `settings-divider` | The line above the switches. |
| `settings-switch-row` | A setting that is on or off: its name and its switch. |
| `settings-switch-label` | That setting's name. |
| `settings-switch` | The switch. Its state is in `data-state`: `checked` or `unchecked`. |
| `settings-switch-thumb` | The dot that slides in the switch. |

### ShareActions

`ShareActionsSlot`, on the [share row](https://bettertyped.github.io/react-wrapped/docs/player/share-actions/).

| Slot | What it is |
| --- | --- |
| `share` | The root: the row's own box. `className` lands here too. |
| `share-copy` | The button that copies the link. |
| `share-download` | The button that makes the video file. |
| `share-stop` | What the download button turns into while the file is made: it counts up, and stops when pressed. |
| `share-progress` | The line under the buttons that says what is being made and how long is left. |
| `share-problem` | The line under the buttons that says why a file could not be made. |

### SceneEditor

`SceneEditorSlot`, on the [scene editor](https://bettertyped.github.io/react-wrapped/docs/player/scene-editor/).

| Slot | What it is |
| --- | --- |
| `editor` | The dialog. Its backdrop is this element's `::backdrop`. `className` lands here too. |
| `editor-header` | The top of the dialog: the icon, the title and the line under it. |
| `editor-icon` | The disc with the pencil in it. |
| `editor-title` | The dialog's title. |
| `editor-description` | The line under the title. |
| `editor-body` | Everything between the header and the buttons. |
| `editor-field` | One thing to edit: its name and its input. |
| `editor-label` | A field's name. |
| `editor-input` | A text input, one line or several. |
| `editor-cut` | The transition picker: the preview beside the choices. |
| `editor-preview` | The box around the transition's preview. |
| `editor-transitions` | The row of transitions. |
| `editor-transition` | One transition. It is a radio: the picked one has `aria-checked`. |
| `editor-footer` | The row of buttons at the bottom. |
| `editor-reset` | The button that restores the original scene. |
| `editor-save` | The button that saves. |

### YearScan

`YearScanSlot`, on the [year scan](https://bettertyped.github.io/react-wrapped/docs/scanner/year-scan/).

| Slot | What it is |
| --- | --- |
| `scan` | The root: the panel. Its background and text colour are the palette's, set inline. `className` lands here too. |
| `scan-header` | The top of the panel: the words on one side, the buttons on the other. |
| `scan-headline` | The headline, which changes with what the scan is doing. |
| `scan-about` | The line under the headline. |
| `scan-actions` | The buttons in the header: yours from `actions`, then the scan's own. |
| `scan-start` | The button that starts the scan, or picks it up again. Its colours are set inline. |
| `scan-find-all` | Shown while a scan runs: sends it straight to its end. |
| `scan-again` | Shown once everything is found: runs the scan again. |
| `scan-grid` | The year: a cell per day. |
| `scan-cell` | One day. Its colour is the palette's level for that day, set inline. |
| `scan-tile` | The button over a cell something was found in. |
| `scan-finds` | The list under the year, a badge per hidden scene. |
| `scan-find` | A found scene's badge. Its colours are set inline, from its rarity. |
| `scan-find-hidden` | The badge of a scene that is still hidden. |
| `scan-find-art` | A badge's little picture. |
| `scan-find-name` | A badge's name. |
| `scan-find-day` | The day a badge was found on. |
| `scan-flight` | What a found card flies in. It covers the page and is drawn at the end of `body`, outside the panel. |
| `scan-backdrop` | The dimmed page behind a card. |
| `scan-card` | The card's face. Its colours are set inline, from its rarity. |
| `scan-card-art` | The picture on the card. |
| `scan-card-name` | The name on the card. |
| `scan-card-rarity` | How rare it is. |
| `scan-card-line` | The line that says what it was earned for. |
| `scan-card-day` | The day it was found on. |
| `scan-card-back` | The card's back, shown before it turns over. |
| `scan-card-button` | The button that covers the card: it turns it over, adds it to the video, or closes it. |

### GraphicsGallery

`GraphicsGallerySlot`, on the [gallery](https://bettertyped.github.io/react-wrapped/docs/graphics/gallery/).

| Slot | What it is |
| --- | --- |
| `gallery` | The root: the heading, the line under it and the tiles. `className` lands here too. |
| `gallery-heading` | The heading. |
| `gallery-heading-accent` | The heading's second half, in the accent colour. |
| `gallery-about` | The line under the heading. |
| `gallery-grid` | The grid of tiles. |
| `gallery-tile` | A graphic that can be taken. Its background and text colour are the palette's, set inline. |
| `gallery-art` | The space the picture stands in. |
| `gallery-image` | The picture. |
| `gallery-placeholder` | The dot that waits where a picture is still being drawn. |
| `gallery-title` | A tile's title. |
| `gallery-line` | The line under the title. |
| `gallery-actions` | The row of buttons on a tile. |
| `gallery-download` | The download button. Its colours are set inline. |
| `gallery-markdown` | The button that copies the Markdown snippet. Its colours are set inline. |
| `gallery-problem` | Said under a tile whose picture did not come through. |
| `gallery-locked` | A graphic the scan has not found yet. |
| `gallery-lock` | The disc with the lock in it. |

### WrappedView

`WrappedViewSlot` is the page's own layout. The component takes every slot on this page in one `classNames` object and hands each part its own.

| Slot | What it is |
| --- | --- |
| `view` | The root: the grid the whole page is laid out on. `className` lands here too. |
| `view-player-panel` | The card the player stands in. |
| `view-side` | The creator's column beside the video: the share row over the settings. |
| `view-settings-panel` | The card the settings stand in. The share row above it is a card by itself: its slot is `share`. |
| `view-viewer` | What holds your `viewer` content, under the video, for someone who is not the creator. |
| `view-scan` | What holds the year scan. |
| `view-gallery` | What holds the gallery. |

## Related

- [Custom palette and chrome colours](https://bettertyped.github.io/react-wrapped/docs/guides/palette-and-chrome/)
- [Labels and translation](https://bettertyped.github.io/react-wrapped/docs/guides/labels/)
- [Theming](https://bettertyped.github.io/react-wrapped/docs/theming/)
- [WrappedPlayer](https://bettertyped.github.io/react-wrapped/docs/player/wrapped-player/)
- [Build the page from parts](https://bettertyped.github.io/react-wrapped/docs/guides/from-parts/)

Source: https://bettertyped.github.io/react-wrapped/docs/guides/styling/
