UNPKG

raft-ui

Version:

React UI components for Raft.

164 lines (105 loc) • 6.76 kB
--- name: styling description: "Styling contract: oklch semantic token families, banned palettes, interaction color pairs, tv() recipe conventions, the data-slot protocol, the var-first shadow trap." --- # Styling Use theme variables directly in plain CSS. The utility and recipe examples below apply to Tailwind consumers. ## Contents - Tailwind's default palette is banned - Semantic token families - Interaction colors come in pairs - When a raw oklch literal is allowed - Controls cast no shadow - Never write a var-first shadow - Writing your own `tv()` recipe - Styling by `data-slot` - Merging classes --- ## Tailwind's default palette is banned Raft-ui's token contract is authored in oklch and exposed through semantic tokens. Tailwind's built-in palette is not part of that contract and will not track the light and dark modes. **Incorrect:** ```tsx <div className="bg-slate-50 text-gray-500 border-neutral-200" /> ``` **Correct:** ```tsx <div className="border border-line-muted bg-fill-muted text-foreground-muted" /> ``` Never write `hex`, `rgb()`, or `hsl()` either. --- ## Semantic token families Reach for these first. The token contract lives in `raft-ui/foundation.css`; `raft-ui/styles.css` maps it to Tailwind utilities. | Family | Purpose | Registered suffixes | | -------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `layer-*` | elevation surfaces | `canvas` `canvas-muted` `panel` `popover` `backdrop` `inset` `card` `hud` `hud-foreground` | | `fill-*` | opaque neutral fills | `muted` `strong` | | `line-*` | borders | bare `line`, then `strong` `muted` `hairline` `field` `field-hover` | | `foreground-*` | text and icons | bare `foreground`, then `strong` `muted` `hint` `icon` `placeholder` `disabled` `inverse` `active` `hover` | | `ink-*` | neutral washes on transparent surfaces | bare `ink`, then `2` `4` `6` `8` `10` `16` `20` `30` `40` | State families do not share one uniform suffix set. `primary` and `accent` expose numeric ramps plus `strong`, `soft`, `hover`, and `active`; `info` exposes `strong`, `muted`, `soft`, `hover`, and `active`; `success` exposes `foreground`, `strong`, `muted`, and `soft`; `warning` and `danger` expose `foreground`, `strong`, `muted`, `soft`, `hover`, and `active`. Check `foundation.css` for values and `styles.css` for utility names. The `ink` ramp step is the alpha percentage — `ink-8` is 8% ink. Use it for hairlines, hover fills, and pressed states on transparent backgrounds. Within the state families, `primary` and `accent` split roles rather than rank: - **`primary-*` (Source Yellow) — hints and interactive expression.** New-message indicators, attention badges, the active tab. The tabs active state draws from the primary family. Focus rings do not: they are ink, a 2px paper gap plus a 2px ink ring (`shadow-raft-focus`, `--theme-focus-ring`). - **`accent-*` — core and important actions.** A dialog's confirm, the composer's send. `ComposerSubmit` locks `variant="accent"` at the type level. Do not use yellow for an action button just because it is the brand color, and do not use accent for a passive indicator. Opaque tokens and alpha washes composite differently on non-white backdrops. Never swap one for the other during a refactor without checking the actual background. --- ## Interaction colors come in pairs Hover and active states on solid fills are derived tokens, not opacity hacks. ```tsx <button className="bg-primary hover:bg-primary-hover active:bg-primary-active" /> ``` Not every state family ships the full set — check `foundation.css` before assuming `success-hover` exists. Adding a family to the interaction contract means defining its complete hover/active pair in the token layer, not patching one callsite. --- ## When a raw oklch literal is allowed One-off precision values — a tuned tint, a glass surface, a terminal black — may be written inline: ```tsx <div className="bg-[oklch(0.21_0.006_285)]" /> ``` Tokenize on the rule of three: once the same value appears in a third component, it earns a token. Do not invent a token for a single consumer. --- ## Controls cast no shadow `--theme-shadow-xs`, `-sm`, and `-md` are empty: controls never cast a shadow. `-lg`, `-xl`, and `-float` are the overlay shadow; `--theme-shadow-sticker` is a hard offset under a sticker, such as the `xl` avatar. --- ## Never write a var-first shadow ```tsx // Incorrect — tailwind-merge misreads this as a shadow-color and conflict // resolution silently breaks. <div className="shadow-[var(--theme-shadow-sm),0_1px_2px_oklch(0_0_0_/_0.18)]" /> ``` Use a registered named shadow utility such as `shadow-raft-sm`, or keep the shadow fully literal: ```tsx <div className="shadow-[0_1px_2px_oklch(0_0_0_/_0.18)]" /> ``` --- ## Writing your own `tv()` recipe **Never construct Tailwind utilities dynamically.** Tailwind's static extraction cannot see them, and the class silently does nothing: ```ts // Broken — the class is never generated. const padding = `px-${size}`; ``` Use static recipe entries (a `size` variant with literal classes) instead. --- ## Styling by `data-slot` Raft-ui's DOM-rendering parts stamp stable `data-slot` attributes (`data-slot="dialog-content"`, `data-slot="banner-action"`, …). Behavior-only roots and providers that render no element have no slot. Recipes use this DOM protocol for structural relationships (`has-[[data-slot=…]]`). When your styling depends on a part's presence or identity, target `data-slot`, never the part's class names: ```tsx // React to a slot's presence from a parent you control: <div className="grid has-[[data-slot=banner-action]]:grid-cols-[1fr_auto]"> ``` Class names are recipe internals and change between releases; `data-slot` values are the stable contract. --- ## Merging classes ```tsx import { cn } from "raft-ui/cn"; ``` `cn` is exported from the package. Do not add `clsx` or `tailwind-merge` as direct dependencies to compose raft-ui classes. Conditional classes go through `cn`, not template-literal ternaries: ```tsx // Incorrect <div className={`px-3 ${active ? "bg-fill-muted" : ""}`} /> // Correct <div className={cn("px-3", active && "bg-fill-muted")} /> ```