Styling & Tokens
framework injects a CSS base layer via the Nuxt module: reset, responsive utilities, design tokens (CSS variables), and an optional dark-theme override. The tokens are the single source of truth — native CSS var(--primary) and Tailwind's bg-primary share the same underlying variables, so overriding a variable affects both at once.
This page covers "which tokens exist, how to use them, how they map to Tailwind". For coding conventions (CSS class naming / unit constraints / zero-tolerance on hardcoded values) see Dev Standards · CSS; for theme switching (light / dark / follow-system) see Theme System.
CSS base layer & injection
Positioning: baseline styles, not zero styles (framework#210) — swift-web components are not zero-style headless; every component ships a minimal usable baseline by default (no collapsed/sticking layout, correct overlay positioning, distinguishable state feedback). "Ugly but usable" passes. Visual details are overridden via project tokens/CSS.
framework ships 9 stylesheets in the npm package, injected by the module per swiftConfig:
| File | Purpose | Injected when |
|---|---|---|
tokens.css | :root design tokens (single source of truth) + base element styles | Always (no switch) |
reset.css | Minimal reset | styles.reset !== false (default on) |
responsive.css | Breakpoints + .sw-* utilities + touch tweaks | styles.responsive !== false (default on) |
theme.css | Dark token overrides (@media + [data-theme]) | theme.mode is system / toggle (fixed dark / light inject the corresponding color-scheme declaration instead) |
color-scheme-dark.css | Declares color-scheme: dark so native browser UI (scrollbars / form controls, etc.) matches a fixed-dark site (the dark palette itself comes from the applier's overrides.css) | theme.mode is dark |
color-scheme-light.css | Declares color-scheme: light, locking native browser UI to light even when the OS is in dark mode | theme.mode is light |
modal.css | SwModal default skin (--sw-modal-* mapped to semantic tokens) | styles.modal !== false (default on) |
toast.css | SwToast default skin (--sw-toast-* mapped to semantic tokens) | styles.toast !== false (default on) |
tailwind-preset.css | @theme inline mapping tokens to the Tailwind namespace | @imported on your side when Tailwind is enabled |
Injection order: framework first, then your overrides.css / custom.css appended via css[] in nuxt.config.ts — so yours wins. Switches: see module options · styles.
Token reference
All tokens live in :root of tokens.css. Reference them directly with var(--x), no fallback — the :root defaults already cover it. Dark values are applied by theme.css when theme.mode is system / toggle (manually locked [data-theme='dark'] or system prefers-color-scheme: dark); in fixed dark mode the applier defines the dark palette directly in :root.
Colors
Semantic names + foreground pairing (Shadcn v4 convention): --x is the background / surface color, --x-foreground is the text on top of it.
| Variable | Purpose | Light | Dark |
|---|---|---|---|
--background | Page background | #ffffff | #0f172a |
--foreground | Page body text | #1a1a1a | #f1f5f9 |
--foreground-soft | Softened body text (a third text level between --foreground and --muted-foreground) | #4b5563 | #cbd5e1 |
--card | Card background | #ffffff | #1e293b |
--card-foreground | Text in cards | #1a1a1a | #f1f5f9 |
--popover | Popover background | #ffffff | #1e293b |
--popover-foreground | Popover text | #1a1a1a | #f1f5f9 |
--primary | Primary color | #3b82f6 | #60a5fa |
--primary-foreground | Text on primary | #ffffff | #0f172a |
--gradient-brand | Brand gradient placeholder (consume via background-image: var(--gradient-brand) on CTAs; defaults to a degenerate gradient with both stops at the primary color = visually solid — brands whose primary is a gradient override it once and it applies site-wide) | linear-gradient(135deg, var(--primary), var(--primary)) | Follows --primary |
--secondary | Secondary background | #f3f4f6 | #334155 |
--secondary-foreground | Secondary text | #1a1a1a | #f1f5f9 |
--accent | Accent (hover / active surface) | #f3f4f6 | #334155 |
--accent-foreground | Accent text | #1a1a1a | #f1f5f9 |
--muted | Muted background | #f8f9fa | #1e293b |
--muted-foreground | Muted text | #6b7280 | #94a3b8 |
--destructive | Danger / error | #ef4444 | #f87171 |
--destructive-foreground | Danger text | #ffffff | #0f172a |
--success | Success | #22c55e | #4ade80 |
--success-foreground | Success text | #ffffff | #0f172a |
--warning | Warning | #f59e0b | #fbbf24 |
--warning-foreground | Warning text | #ffffff | #0f172a |
--info | Info | #3b82f6 | #60a5fa |
--info-foreground | Info text | #ffffff | #0f172a |
--border | Generic border | #e5e7eb | #334155 |
--input | Form border | #e5e7eb | #334155 |
--ring | Focus ring | #3b82f6 | #60a5fa |
Typography
| Variable | Default |
|---|---|
--font-sans | system-ui, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Segoe UI', Roboto, Arial, sans-serif |
--font-mono | 'SF Mono', 'Cascadia Code', Consolas, monospace |
Radius & shadow
| Variable | Default | Note |
|---|---|---|
--radius | 0.5rem | Radius baseline — the one businesses tweak most |
--shadow-sm | 0 1px 3px rgb(0 0 0 / 8%) | Small shadow |
--shadow-md | 0 4px 12px rgb(0 0 0 / 12%) | Medium shadow |
--shadow-lg | 0 10px 30px rgb(0 0 0 / 16%) | Large shadow |
--scrim | linear-gradient(transparent, rgb(0 0 0 / 68%)) | Bottom cover scrim (keeps overlay text readable — a media-site staple; always dark, theme-invariant. Consume with a single background: var(--scrim), no no-hardcode disable needed. Composite artistic hero scrims are out of scope — brand territory, app-owned) |
--scrim-top | linear-gradient(rgb(0 0 0 / 45%), transparent) | Top cover scrim (player top bar / header-over-image scenarios) |
Form controls
| Variable | Default | Notes |
|---|---|---|
--control-height | 2rem | Unified form-control height (SwInput / SwSelect / SwSegmented align by default; override to scale every control site-wide) |
--control-font-size | 0.875rem | Unified form-control font size (SwInput bumps back to 1rem on touch devices to avoid the iOS focus-zoom on <16px inputs) |
Layout
| Variable | Default | Note |
|---|---|---|
--nav-height | 60px (mobile 56px) | Top nav height |
--container-padding | 24px (mobile 16px; ≥1280 48px, ≥1600 96px) | Container horizontal padding — wide-screen margins come from this, not from --content-max-width |
--content-max-width | 1920px | Max content width (content-site baseline, fw#283 — full-bleed on a 1920 screen, centred with margins at 2560; sites wanting a narrow container set 1200px back in overrides) |
Not included: spacing (
--space-4etc.), fontSize / fontWeight / lineHeight, color scales (--gray-50–--gray-900). With Tailwind usep-4/text-lg; in native CSS, raw values are more flexible. framework is not an all-in-one token store.A few component-internal tokens also exist (e.g. SwImage's
--sw-image-placeholder-bg); those are for tuning that component only, not part of the general palette.
Component skin variables (modal / toast)
The default skins of SwModal / SwToast are injected by the framework (modal.css / toast.css). Every skin variable defaults to a semantic token above — changing a semantic token updates the whole site (dialogs / toasts included), while overriding the variables below affects only that component. Override them at a top-level :root in overrides.css (both teleport to body, so scoped styles cannot reach them). Each skin can be disabled via framework.styles.modal / styles.toast set to false.
--sw-modal-* (SwModal / SwModalHost)
| Variable | Default | Description |
|---|---|---|
--sw-modal-overlay-bg | rgb(0 0 0 / 45%) | Overlay color (fixed, theme-invariant) |
--sw-modal-bg | var(--background) | Dialog background |
--sw-modal-border | var(--border) | Dialog border |
--sw-modal-shadow | var(--shadow-lg) | Dialog shadow |
--sw-modal-radius | 0.75rem | Dialog radius |
--sw-modal-width | 28rem (inline fallback) | Dialog width (actual min(90vw, …)) |
--sw-modal-title-color | var(--foreground) | Title color |
--sw-modal-desc-color | var(--muted-foreground) | Description color |
--sw-modal-text-color | var(--foreground) | Body color |
--sw-modal-close-color | var(--muted-foreground) | Close ✕ color |
--sw-modal-close-bg | transparent | ✕ background (solid color + radius: 50% → round filled button) |
--sw-modal-close-radius | 6px | ✕ corner radius (set 50% for a circle) |
--sw-modal-close-size | 1.75rem | ✕ square size |
--sw-modal-close-hover-bg | var(--muted) | ✕ hover background |
--sw-modal-close-hover-color | var(--foreground) | ✕ hover color |
--sw-modal-footer-border | var(--border) | Footer divider |
--sw-modal-btn-cancel-bg | var(--background) | Cancel button background |
--sw-modal-btn-cancel-border | var(--border) | Cancel button border |
--sw-modal-btn-cancel-color | var(--foreground) | Cancel button text |
--sw-modal-btn-cancel-hover-bg | var(--muted) | Cancel hover background |
--sw-modal-btn-confirm-bg | var(--primary) | Confirm button background (hover via filter: brightness) |
--sw-modal-btn-confirm-color | var(--primary-foreground) | Confirm button text |
--sw-toast-* (SwToast / useToast)
| Variable | Default | Description |
|---|---|---|
--sw-toast-bg | color-mix(in srgb, var(--background) 92%, transparent) | Toast background (frosted translucent) |
--sw-toast-color | var(--foreground) | Text color |
--sw-toast-border | var(--border) | Border |
--sw-toast-shadow | var(--shadow-md) | Shadow |
--sw-toast-close-color | var(--muted-foreground) | Close ✕ color |
--sw-toast-close-hover-bg | var(--muted) | ✕ hover background |
--sw-toast-close-hover-color | var(--foreground) | ✕ hover color |
--sw-toast-icon-success | var(--success) | ✓ icon color |
--sw-toast-icon-error | var(--destructive) | ✕ icon color |
--sw-toast-icon-warning | var(--warning) | ! icon color |
--sw-toast-icon-info | var(--info) | i icon color |
One source for native CSS and Tailwind
tailwind-preset.css uses Tailwind v4's @theme inline to remap every :root token into the Tailwind namespace — it defines no new values, only mappings. So one token set drives both styles:
/* Native CSS */
.cta {
background: var(--primary);
color: var(--primary-foreground);
border-radius: var(--radius);
}
<!-- Tailwind utilities (equivalent) -->
<button class="bg-primary text-primary-foreground rounded-lg">Order</button>
After enabling Tailwind (sw config set features.tailwind true, see CLI), your tailwind.css:
@import 'tailwindcss';
@import '@swift-web/framework/styles/tailwind-preset.css';
On dark switch both styles follow automatically — no dark: prefix needed — because the underlying var() is already overridden by theme.css.
Mapping table
framework token (native var()) | Tailwind utility |
|---|---|
--background / --foreground | bg-background / text-foreground |
--card / --card-foreground | bg-card / text-card-foreground |
--popover / --popover-foreground | bg-popover / text-popover-foreground |
--primary / --primary-foreground | bg-primary / text-primary-foreground / border-primary |
--secondary / --accent / --muted (with their foregrounds) | bg-secondary / bg-accent / bg-muted … (same pattern) |
--destructive / --success / --warning / --info | bg-destructive / text-success … (same pattern) |
--border / --input / --ring | border-border / border-input / ring-ring |
--radius (derives sm/md/lg/xl) | rounded-sm / rounded-md / rounded-lg / rounded-xl |
--font-sans / --font-mono | font-sans / font-mono |
--shadow-sm / --shadow-md / --shadow-lg | shadow-sm / shadow-md / shadow-lg |
Breakpoints 768px / 1024px | sm: / md: prefixes |
Radius note: native CSS has only the single
var(--radius);rounded-sm/md/lg/xlare derived by the preset from--radiusviacalc()(override--radiusto scale them all).Don't reference
var(--color-primary)in hand-written CSS:--color-*are bridge variables the preset exposes for Tailwind (= var(--primary)). Referencing them in hand-written CSS is the wrong layer — always use the semantic tokenvar(--primary).
Overriding defaults
Override only in :root of app/assets/styles/overrides.css; both native and Tailwind follow:
/* app/assets/styles/overrides.css */
:root {
--primary: #6366f1;
--radius: 0.75rem;
}
/* Dark override (when theme.mode is system / toggle) */
:root[data-theme='dark'] {
--primary: #818cf8;
}
Use prefixes like --brand-* / --page-* for your own tokens to stay clear of the framework namespace, defined centrally in a project-level :root (also without fallback). See Theme System · Custom theme variables.
Responsive utilities
responsive.css provides breakpoint conventions plus a set of .sw--prefixed utilities (the prefix avoids clashing with Tailwind's .container etc.).
Breakpoints (Mobile-first):
| Breakpoint | Range |
|---|---|
| Mobile | < 768px |
| Tablet | 768px – 1023px |
| Desktop | ≥ 1024px |
Minimum supported width: 360px (the most common narrow Android width, framework#446): framework component e2e tests run at 360 on mobile. Below 360 (320 on iPhone SE / mini with Display Zoom, foldable cover screens) is not guaranteed and not gated.
Utilities:
| Class | Effect |
|---|---|
.sw-container | Responsive container: max-width: var(--content-max-width) + centered + horizontal var(--container-padding) |
.sw-show-mobile | Shown only < 768px (hidden otherwise) |
.sw-hide-mobile | Hidden < 768px |
.sw-hide-tablet | Hidden 768px – 1023px |
.sw-hide-desktop | Hidden ≥ 1024px |
On mobile (< 768px), --nav-height drops to 56px and --container-padding to 16px; on wide screens it scales up instead (≥1280 → 48px, ≥1600 → 96px, fw#284).
🔴 The two layout variables have different jobs, and this is the easiest thing to get wrong.
--content-max-widthsets "where the container stops growing" — it only matters on 2K/4K displays, because the default of 1920 is never reached on a 1920 screen (the container is effectively full-bleed there).--container-paddingsets "how much margin there is at any width" — every bit of whitespace you see on a wide screen comes from it. To narrow the content area you must change both: changing only--content-max-widthleaves the 96px padding in place and squeezes the already-narrowed container a second time (set 1280 and you actually get 1088).
responsive.css deliberately does not ship any global min-height for a / button / [role=button] (removed in framework#265): the specs constrain the hit area, not the visual box (WCAG defines a target as the "region of the display that will accept a pointer action"; Material Design 3 states touch targets "extend beyond the visual bounds of an element"), the conformance baseline is 24×24 from WCAG 2.5.8 AA (44×44 comes from AAA 2.5.5), and inline text links are explicitly exempt under both SCs. Visual size is up to each component; use sw-hit-area below to enlarge the hit area.
Compact-control hit-area expansion (opt-in): sw-hit-area — for buttons / links intentionally smaller than 44px (round close ✕, chip remove buttons, icon buttons), add the sw-hit-area class: a pseudo-element expands the tap area to 44×44 while the visuals stay exactly the same (the industry-recommended pattern: position: relative + pseudo-element + pointer: coarse gating). framework's built-in compact icon buttons are already onboarded inside the components (SwThemeSwitch / SwPagination / SwAdPause close / SwEditorToolbar tools / modal close / SwSelect chip ✕ / carousel dots etc.), so applications never need a touch-side patch when using Sw* components; add it yourself on controls you build. Notes: ① the host element must not use ::before itself; ② do not add it to dense grids — an emoji picker (8 columns / 2px gap) expanded to 44×44 would overlap neighbouring cells and cause mis-taps.
<!-- 2rem round icon button: stays a circle, 44×44 tap area -->
<SwButton class="close-btn sw-hit-area" aria-label="Close">…</SwButton>
FAQ
Why --primary instead of --color-primary?
Following the Shadcn v4 convention — native var(--primary) is shorter than var(--color-primary) and just as clear. Tailwind's @theme inline remaps it to --color-primary in the Tailwind namespace, transparently to you.
Are these tokens usable in fixed light mode (theme.mode: 'light')?
Yes. tokens.css is always injected; you just get no dark overrides — a pure light theme.
How do I add my own tokens?
Add prefixed variables (--brand-* etc.) under :root in overrides.css / custom.css. For Tailwind utilities, add @theme { --color-brand: ...; } in your own tailwind.css.
Why no spacing / fontSize tokens?
Tailwind ships its own (p-4 / text-lg); in native CSS raw values are more flexible. framework is not an all-in-one token store, to avoid burdening downstream.