Skip to content
react-wrapped

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.

The same player and settings with and without a class per slot. Square buttons, outlined chips.

own-style.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.

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` 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
@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` has the same slots, so the frame and the controls look the same before the year arrives as after.

SlotWhat it is
playerThe root: the video and the controls under it. className lands here too.
player-roomThe space the frame is fitted into.
player-frameThe frame around the video: its corners, its shadow, its outline.
player-transportThe row of controls under the video.
player-playThe play and pause button.
player-restartThe button that plays from the start.
player-clockWhere the playhead is and how long the video runs.
player-fullscreenThe full screen button.

Timeline

TimelineSlot. The 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.

SlotWhat it is
timelineThe root: the row of segments and the preview that opens over it.
timeline-trackThe row of segments, the part that is clicked and dragged.
timeline-segmentOne scene's share of the row.
timeline-segment-barThe bar drawn inside a segment.
timeline-segment-fillThe played part of a bar.
timeline-popoverWhat holds the preview over its segment. Its z-index is set here.
timeline-previewThe preview card.
timeline-preview-thumbThe box around the still of the scene.
timeline-preview-titleThe scene's name.
timeline-preview-lengthHow long the scene plays, or that it is off.
timeline-preview-rarityHow rare a found scene is. Its colour is the palette's, set inline.
timeline-controlsThe creator's row of controls in the card.
timeline-move-earlierThe arrow that moves the scene one place earlier.
timeline-move-laterThe arrow that moves it one place later.
timeline-editThe pencil that opens the scene editor.
timeline-toggleThe switch that takes the scene out of the video.
timeline-toggle-thumbThe dot that slides in that switch.

WrappedConfigurator

ConfiguratorSlot, on the settings panel.

SlotWhat it is
settingsThe root. className lands here too.
settings-fieldOne setting: its name and what there is to pick.
settings-labelA setting's name.
settings-paletteThe grid of palettes.
settings-swatchOne palette in the grid. It is a radio: the picked one has aria-checked.
settings-swatch-colorsThe palette's five colours on its swatch.
settings-swatch-colorOne of the five.
settings-swatch-nameThe palette's name.
settings-accentThe row for a colour of your own, shown while the custom palette is picked.
settings-accent-inputThe colour input in that row.
settings-accent-randomThe button that picks a random colour.
settings-chipsA row of choices: dark or light, format, cell shape, speed.
settings-chipOne choice. It is a radio: the picked one has aria-checked.
settings-dividerThe line above the switches.
settings-switch-rowA setting that is on or off: its name and its switch.
settings-switch-labelThat setting's name.
settings-switchThe switch. Its state is in data-state: checked or unchecked.
settings-switch-thumbThe dot that slides in the switch.

ShareActions

ShareActionsSlot, on the share row.

SlotWhat it is
shareThe root: the row's own box. className lands here too.
share-copyThe button that copies the link.
share-downloadThe button that makes the video file.
share-stopWhat the download button turns into while the file is made: it counts up, and stops when pressed.
share-progressThe line under the buttons that says what is being made and how long is left.
share-problemThe line under the buttons that says why a file could not be made.

SceneEditor

SceneEditorSlot, on the scene editor.

SlotWhat it is
editorThe dialog. Its backdrop is this element's ::backdrop. className lands here too.
editor-headerThe top of the dialog: the icon, the title and the line under it.
editor-iconThe disc with the pencil in it.
editor-titleThe dialog's title.
editor-descriptionThe line under the title.
editor-bodyEverything between the header and the buttons.
editor-fieldOne thing to edit: its name and its input.
editor-labelA field's name.
editor-inputA text input, one line or several.
editor-cutThe transition picker: the preview beside the choices.
editor-previewThe box around the transition's preview.
editor-transitionsThe row of transitions.
editor-transitionOne transition. It is a radio: the picked one has aria-checked.
editor-footerThe row of buttons at the bottom.
editor-resetThe button that restores the original scene.
editor-saveThe button that saves.

YearScan

YearScanSlot, on the year scan.

SlotWhat it is
scanThe root: the panel. Its background and text colour are the palette's, set inline. className lands here too.
scan-headerThe top of the panel: the words on one side, the buttons on the other.
scan-headlineThe headline, which changes with what the scan is doing.
scan-aboutThe line under the headline.
scan-actionsThe buttons in the header: yours from actions, then the scan's own.
scan-startThe button that starts the scan, or picks it up again. Its colours are set inline.
scan-find-allShown while a scan runs: sends it straight to its end.
scan-againShown once everything is found: runs the scan again.
scan-gridThe year: a cell per day.
scan-cellOne day. Its colour is the palette's level for that day, set inline.
scan-tileThe button over a cell something was found in.
scan-findsThe list under the year, a badge per hidden scene.
scan-findA found scene's badge. Its colours are set inline, from its rarity.
scan-find-hiddenThe badge of a scene that is still hidden.
scan-find-artA badge's little picture.
scan-find-nameA badge's name.
scan-find-dayThe day a badge was found on.
scan-flightWhat a found card flies in. It covers the page and is drawn at the end of body, outside the panel.
scan-backdropThe dimmed page behind a card.
scan-cardThe card's face. Its colours are set inline, from its rarity.
scan-card-artThe picture on the card.
scan-card-nameThe name on the card.
scan-card-rarityHow rare it is.
scan-card-lineThe line that says what it was earned for.
scan-card-dayThe day it was found on.
scan-card-backThe card's back, shown before it turns over.
scan-card-buttonThe button that covers the card: it turns it over, adds it to the video, or closes it.

GraphicsGallery

GraphicsGallerySlot, on the gallery.

SlotWhat it is
galleryThe root: the heading, the line under it and the tiles. className lands here too.
gallery-headingThe heading.
gallery-heading-accentThe heading's second half, in the accent colour.
gallery-aboutThe line under the heading.
gallery-gridThe grid of tiles.
gallery-tileA graphic that can be taken. Its background and text colour are the palette's, set inline.
gallery-artThe space the picture stands in.
gallery-imageThe picture.
gallery-placeholderThe dot that waits where a picture is still being drawn.
gallery-titleA tile's title.
gallery-lineThe line under the title.
gallery-actionsThe row of buttons on a tile.
gallery-downloadThe download button. Its colours are set inline.
gallery-markdownThe button that copies the Markdown snippet. Its colours are set inline.
gallery-problemSaid under a tile whose picture did not come through.
gallery-lockedA graphic the scan has not found yet.
gallery-lockThe 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.

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

Last updated . Read this page as Markdown.