Skip to content

Latest commit

 

History

History
226 lines (186 loc) · 14.1 KB

File metadata and controls

226 lines (186 loc) · 14.1 KB

Theming Token Map

Audience: contributors changing UI styles, components, or page layouts.

This project uses two theming layers:

  • CSS custom properties in app/globals.css for global browser-level values.
  • Tailwind theme extensions in tailwind.config.js for component styling.

Prefer these tokens before adding raw colors, spacing, focus rings, breakpoints, or animation values. If a new value is needed, add it to the token layer first and document its semantic role here in the same PR. For surface hierarchy and shadow use, see docs/ELEVATION.md, and check the Design System Roadmap for active token deprecations and planned components.

Theme-Switching Architecture

This section documents how a ThemePreference ("system" | "light" | "dark") actually gets from storage to the rendered page. The token catalog above is what the tokens are; this is how the app picks which values apply.

The moving parts

  • lib/config/theme.ts — the ThemePreference type, the THEME_STORAGE_KEY ("theme-preference") it's persisted under in localStorage, and the isThemePreference guard used to validate whatever comes back out of storage (never trust it blindly -- it can be corrupted, stale from an older schema, or absent).
  • lib/context/ThemeContext.tsxThemeProvider/useTheme. Owns the current preference in React state, seeded from localStorage on mount, and exposes setTheme (writes through to localStorage and updates state).
  • The <html> element's class list.dark / .light (or neither, for the OS-driven case). This, not React state, is what the CSS custom properties in :root / html.dark / html.light actually key off of. See applyThemePreference() in ThemeContext.tsx.
  • The inline script in app/layout.tsx — a second, hand-duplicated copy of the same class-resolution logic as applyThemePreference, injected as a raw <script> and run before React hydrates.

Why there are two copies of the same logic

The inline script in app/layout.tsx exists purely to avoid a flash of the wrong theme: without it, the page would first paint with no theme class (the server has no access to localStorage), then flip to the right one only once ThemeProvider's effect runs after hydration. Running the class resolution synchronously, before paint, eliminates that flash.

This means app/layout.tsx's inline script and ThemeContext.tsx's applyThemePreference must be kept in sync by hand -- there is no shared module between them (the inline script can't import anything; it has to be a self-contained string). If you change how a preference resolves to a class in one, change it in the other in the same PR, or the pre-hydration paint and the post-hydration state will disagree.

"system" stays live

Choosing "system" doesn't just resolve prefers-color-scheme once. ThemeContext.tsx's effect subscribes to the media query's change event for as long as theme === "system", so toggling the OS theme while the app is open updates the page immediately, with no reload. Switching away from "system" unsubscribes (the effect's cleanup runs on the next theme change).

CSS Custom Properties

All current CSS custom properties are declared in app/globals.css.

Property Default value Dark-mode value Semantic role Current usage
--foreground-rgb 0, 0, 0 255, 255, 255 Global foreground text color expressed as RGB channels for rgb(var(...)) composition. body { color: rgb(var(--foreground-rgb)); }
--background-start-rgb 214, 219, 220 0, 0, 0 Legacy page-gradient start channel. Keep documented because it is part of the global theme contract, even though the current body background is plain black. Not currently referenced outside declaration.
--background-end-rgb 255, 255, 255 0, 0, 0 Legacy page-gradient end channel. Keep documented with --background-start-rgb for future gradient restoration. Not currently referenced outside declaration.
--background Not declared #010101 Dark application canvas. Use for full-page dark surfaces when a CSS variable is required instead of a Tailwind class. Declared for dark mode; not currently referenced outside declaration.
--color-bg2 Not declared #0f0f0f Upper layer of the dark card gradient. Used by --card.
--color-bg3 Not declared #0a0a0a Lower layer of the dark card gradient. Used by --card.
--card Not declared linear-gradient(var(--color-bg2), var(--color-bg3)) Reusable dark card background gradient. Declared for card-like surfaces that need a CSS variable.
--accent Not declared #dc2626 Primary red accent for dark-mode UI emphasis. Prefer Tailwind brand.red or red.600 in JSX unless CSS needs a variable. Declared for CSS-level accent styling.
--skeleton-base rgba(0, 0, 0, 0.06) rgba(255, 255, 255, 0.05) Resting fill of an animated skeleton placeholder, and the two ends of its shimmer gradient. .rw-skeleton--shimmer
--skeleton-highlight rgba(0, 0, 0, 0.12) rgba(255, 255, 255, 0.1) Travelling highlight at the midpoint of the shimmer gradient. .rw-skeleton--shimmer
--skeleton-static rgba(0, 0, 0, 0.1) rgba(255, 255, 255, 0.1) Flat fill of a non-animated skeleton placeholder — the static variant, and what the shimmer variant falls back to under prefers-reduced-motion: reduce. Set to the shimmer's highlight value, not its base, so removing the animation does not also make the placeholder fainter. .rw-skeleton

The active global body styles are intentionally minimal:

body {
  color: rgb(var(--foreground-rgb));
  background: black;
}

Tailwind Theme Tokens

Tailwind tokens are defined under theme.extend in tailwind.config.js. Use them through class names so reviewers can distinguish semantic values from one-off styling.

Breakpoints

Token Value Semantic role Example
320 320px Smallest supported mobile viewport, including iPhone SE. 320:px-6
375 375px Primary mobile target for modern phones. 375:text-base
450 450px Foldables and larger phones before tablet layout. 450:grid-cols-2
tablet 768px Tablet portrait layouts. tablet:grid-cols-3
laptop 1024px Tablet landscape and small laptop layouts. laptop:px-8
desktop 1440px Full desktop layout width. desktop:text-5xl

Spacing

Token Value Semantic role Example
space-xs 4px Small internal gaps, icon/text spacing. gap-space-xs
space-sm 8px Compact stacked controls or labels. space-y-space-sm
space-md 16px Default component padding or list spacing. p-space-md
space-lg 24px Section-level spacing inside cards or panels. gap-space-lg
space-xl 32px Large section spacing. py-space-xl
3.5 14px Fine-grained padding and control spacing. py-3.5
7 28px Mobile layout spacing between default 6 and 8. 375:gap-7
9 36px Large but not full 10 spacing. tablet:gap-9
11 44px Minimum accessible touch target dimension. h-11 w-11
13 52px Large icon-button or panel spacing. h-13
15 60px Large vertical rhythm. py-15
17.5 70px Fine-grained hero/section spacing. pt-17.5
22.5 90px Wide section spacing. py-22.5
27.5 110px Largest custom section spacing. py-27.5

Focus Rings

Token Value Semantic role Example
ring-focus 3px Accessible focus ring width for important controls. focus-visible:ring-focus
ring-offset-focus 4px Focus ring separation from dark surfaces. focus-visible:ring-offset-focus

Colors

Token Value Semantic role Example
brand.red #D72323 Primary RemitWise action and brand accent. bg-brand-red
brand.dark #0A0A0A Brand dark canvas. bg-brand-dark
brand.redHover #B91C1C Hover state for primary red actions. hover:bg-brand-redHover
primary.50 #f0f9ff Lightest informational blue. bg-primary-50
primary.100 #e0f2fe Very light informational blue. bg-primary-100
primary.200 #bae6fd Light informational blue. bg-primary-200
primary.300 #7dd3fc Soft informational blue. text-primary-300
primary.400 #38bdf8 Medium informational blue. text-primary-400
primary.500 #0ea5e9 Default informational blue. text-primary-500
primary.600 #0284c7 Strong informational blue. bg-primary-600
primary.700 #0369a1 Dark informational blue. bg-primary-700
primary.800 #075985 Darker informational blue. bg-primary-800
primary.900 #0c4a6e Darkest informational blue. bg-primary-900
red.600 #DC2626 Default Tailwind-compatible red action color. bg-red-600
red.700 #B91C1C Red hover/pressed state. hover:bg-red-700
red.800 #991B1B Strong red surface or pressed state. bg-red-800
red.900 #7F1D1D Deep red surface. bg-red-900

Status Colors

Status tokens are grouped by foreground, background, border, and soft surface. Use these instead of hand-rolled status colors so success, warning, error, and info states remain consistent.

Token Value Semantic role Example
status.success.fg #86EFAC Success text or progress fill. text-status-success-fg
status.success.bg rgba(34, 197, 94, 0.14) Success badge or panel background. bg-status-success-bg
status.success.border rgba(34, 197, 94, 0.28) Success border. border-status-success-border
status.success.soft rgba(20, 83, 45, 0.28) Deeper success surface. bg-status-success-soft
status.warning.fg #FDE68A Warning text or icon. text-status-warning-fg
status.warning.bg rgba(245, 158, 11, 0.14) Warning badge or panel background. bg-status-warning-bg
status.warning.border rgba(245, 158, 11, 0.28) Warning border. border-status-warning-border
status.warning.soft rgba(120, 53, 15, 0.28) Deeper warning surface. bg-status-warning-soft
status.error.fg #FDA4AF Error text or icon. text-status-error-fg
status.error.bg rgba(244, 63, 94, 0.14) Error badge or panel background. bg-status-error-bg
status.error.border rgba(244, 63, 94, 0.28) Error border. border-status-error-border
status.error.soft rgba(127, 29, 29, 0.3) Deeper error surface. bg-status-error-soft
status.info.fg #93C5FD Informational text or icon. text-status-info-fg
status.info.bg rgba(59, 130, 246, 0.14) Informational badge or panel background. bg-status-info-bg
status.info.border rgba(59, 130, 246, 0.28) Informational border. border-status-info-border
status.info.soft rgba(30, 64, 175, 0.24) Deeper informational surface. bg-status-info-soft

Animation Tokens

Token Value Semantic role Example
animate-neon-pulse neon-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite Pulsing emphasis for neon/accent elements. animate-neon-pulse
animate-shimmer shimmer 2s linear infinite Skeleton or loading shimmer. animate-shimmer
animate-slide-in-right slide-in-right 0.25s ease-out forwards Drawer or toast entrance from the right. animate-slide-in-right
animate-slide-in-bottom slide-in-bottom 0.25s ease-out forwards Mobile sheet entrance from the bottom. animate-slide-in-bottom

Global Utility Classes

These utilities are defined in app/globals.css.

Utility CSS output Semantic role
.starry-bg Repeating radial dot background at 40px size. Decorative dark-page background.
.safari-safe-top padding-top: env(safe-area-inset-top) Respect iOS top safe area.
.safari-safe-bottom padding-bottom: env(safe-area-inset-bottom) Respect iOS bottom safe area. Requires viewport-fit=cover (set globally in app/layout.tsx) or the inset reads as 0px. Elements with existing base padding should use the calc(theme(spacing.N)+env(...)) pattern instead — see docs/tailwind-extensions.md.
.safari-safe-left padding-left: env(safe-area-inset-left) Respect iOS left safe area.
.safari-safe-right padding-right: env(safe-area-inset-right) Respect iOS right safe area.
.touch-target min-height: 44px; min-width: 44px Minimum accessible square touch target.
.touch-target-wide min-height: 44px; min-width: 88px Minimum accessible wide touch target for buttons.

Two further classes are emitted into Tailwind's components layer, so any utility passed in className still wins over them. Apply them through the <Skeleton /> component rather than by hand — see docs/COMPONENTS.md.

Class CSS output Semantic role
.rw-skeleton background-color: var(--skeleton-static) Static skeleton placeholder; never animates.
.rw-skeleton--shimmer Shimmer gradient from the --skeleton-* tokens, animated by rw-skeleton-shimmer. Reset to the static fill under prefers-reduced-motion: reduce. Animated skeleton placeholder.

Contributor Checklist

  • Use CSS custom properties only for global CSS values that must exist outside Tailwind class composition.
  • Use Tailwind token classes for component styling in JSX.
  • Keep status UI on status.* tokens.
  • Keep primary actions on brand.red, brand.redHover, or the local red scale.
  • Do not introduce new hard-coded colors, spacing, radii, or focus values in a component when a token already represents the same role.
  • Update this file when app/globals.css or tailwind.config.js adds, removes, or changes theme tokens.
  • For contrast ratio requirements and how to verify new colour tokens, see docs/SEMANTIC_TOKENS_AND_CONTRAST.md.