Rules of the road
The conventions that keep every surface converged — theming, responsiveness, and the right way to consume the system from each stack.
Dark mode & the token auto-inversion rule
Dark mode is system-preference by default, with an explicit
<html data-theme="dark|light"> override — never a separate theming
mechanism. The interesting part is how the palette gets there: the token ramps
invert themselves. In dark mode --success-100 resolves to a deep
teal and --success-800 to a pale one, so a light-mode recipe like
"100 background, 800 text" is automatically correct on a dark ground.
See it live
These pills use single token classes — no dark: anything. Toggle the theme (top right): they stay legible because the ramp flips underneath them.
The corollary is the most common mistake in app code: stacking Tailwind
dark: variants on token colors double-inverts. The token already
flipped; the dark: override flips it back (or somewhere worse) and washes
out contrast. This bit two dashboard components before it became a rule.
<span class="bg-success-100 text-success-800">
Stable
</span>
One token recipe. The ramp inversion makes it correct in both themes.
<span class="bg-success-100 text-success-800
dark:bg-success-900 dark:text-success-200">
Stable
</span>
Double inversion. In dark mode this renders pale-on-pale and loses all contrast.
dark:. And when adding a new color, ship both halves:
the 50–950 ramp and its dark inversion, in the same change.
Using tokens
Tokens are RGB triplets, which keeps alpha compositing in the consumer's hands.
/* plain CSS */
color: rgb(var(--primary-600));
background: rgb(var(--primary-500) / 0.16); /* alpha over any ground */
/* Tailwind (preset maps the scales onto tokens) */
<div class="bg-primary-600 text-primary-50">
/* the preset also aliases legacy names during migration:
blue→primary · green/teal→success · red/rose→error
orange/amber/yellow→warning · gray/slate→secondary */#219EFF anywhere
outside tokens.css, stop and use the token.
The responsive contract
Components ship mobile-ready. Apps must not override these down.
44px touch targets
Every interactive element hits at least 44×44px on phones — buttons, nav links, icon buttons.
≥16px mobile inputs
Form text never drops below 16px on mobile, which stops iOS focus-zoom.
Sheets, not floats
Modals become full-width bottom sheets ≤640px; toasts span the viewport width.
Safe areas
Fixed chrome pads with env(safe-area-inset-*) for notched devices.
Fluid type & spacing
Heroes and containers use clamp() so layouts scale instead of snapping.
Focus visibility
Keyboard focus is always visible (azure ring); mouse focus stays quiet.
Consuming the system
React apps
Install from npm, import the stylesheet once, adopt the Tailwind preset, and import components. Auth (@readysetcloud/ui/auth) and chat (@readysetcloud/ui/chat) live on subpaths so you only pull what you use.
import '@readysetcloud/ui/styles.css';
import { AppNav, StatTile, SegmentedControl } from '@readysetcloud/ui';
import { AuthProvider, useAuth } from '@readysetcloud/ui/auth';
Hugo / static sites
Link the hosted stylesheet — or tokens.css alone to adopt the palette while keeping your own components.
<link rel="stylesheet" href="https://<assets-host>/ui/latest/styles/index.css">
<!-- or variables only: -->
<link rel="stylesheet" href="https://<assets-host>/ui/latest/styles/tokens.css">
Vanilla JS
Three script-tag globals cover what CSS can't: auth, the shared navbar, and drawing helpers. Everything else — stat tiles, pills, segmented controls, heroes — is just the shipped classes. Point the tags at the hosted assets bucket (the same S3/CDN host as the stylesheet above), never a local copy — that's how every surface stays on one version.
<script src="https://<assets-host>/ui/latest/auth.global.js"></script> <!-- window.rscAuth -->
<script src="https://<assets-host>/ui/latest/nav.global.js"></script> <!-- window.rscNav -->
<script src="https://<assets-host>/ui/latest/ui.global.js"></script> <!-- window.rscUi -->
<script>
rscAuth.configureAuth({ region: 'us-east-1', clientId: '…' });
rscNav.mountAppNav('#nav', { appName: 'Bootcamp' /* … */ });
rscUi.renderSparkline(document.querySelector('#trend'), [38, 42, 45, 48]);
</script>
Contributing
The system only works if it stays the single source of truth. If an app needs a variant
that doesn't exist, add it to rsc-core/ui first — never fork a component
into an app.
| Checklist for changes | Why |
|---|---|
| Keep class names stable | Apps (and Hugo partials) depend on them directly. |
| Both light and dark values for new tokens | The auto-inversion contract only holds if every ramp defines both halves. |
| Ship the CSS class with the React component | React/CSS parity is what keeps vanilla surfaces pixel-identical. |
| Mobile-first (44px targets, ≥16px inputs) | The responsive contract is part of the component API. |
Bump version in ui/package.json | Deploy publishes hosted assets under the version; CI publishes to npm when it's new. |
design-system/ on every push to main
that touches it or the ui package — the workflow rebuilds the package and republishes,
so docs and package can't drift.