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:

FilePurposeInjected when
tokens.css:root design tokens (single source of truth) + base element stylesAlways (no switch)
reset.cssMinimal resetstyles.reset !== false (default on)
responsive.cssBreakpoints + .sw-* utilities + touch tweaksstyles.responsive !== false (default on)
theme.cssDark token overrides (@media + [data-theme])theme.mode is system / toggle (fixed dark / light inject the corresponding color-scheme declaration instead)
color-scheme-dark.cssDeclares 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.cssDeclares color-scheme: light, locking native browser UI to light even when the OS is in dark modetheme.mode is light
modal.cssSwModal default skin (--sw-modal-* mapped to semantic tokens)styles.modal !== false (default on)
toast.cssSwToast 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.

VariablePurposeLightDark
--backgroundPage background#ffffff#0f172a
--foregroundPage body text#1a1a1a#f1f5f9
--foreground-softSoftened body text (a third text level between --foreground and --muted-foreground)#4b5563#cbd5e1
--cardCard background#ffffff#1e293b
--card-foregroundText in cards#1a1a1a#f1f5f9
--popoverPopover background#ffffff#1e293b
--popover-foregroundPopover text#1a1a1a#f1f5f9
--primaryPrimary color#3b82f6#60a5fa
--primary-foregroundText on primary#ffffff#0f172a
--gradient-brandBrand 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
--secondarySecondary background#f3f4f6#334155
--secondary-foregroundSecondary text#1a1a1a#f1f5f9
--accentAccent (hover / active surface)#f3f4f6#334155
--accent-foregroundAccent text#1a1a1a#f1f5f9
--mutedMuted background#f8f9fa#1e293b
--muted-foregroundMuted text#6b7280#94a3b8
--destructiveDanger / error#ef4444#f87171
--destructive-foregroundDanger text#ffffff#0f172a
--successSuccess#22c55e#4ade80
--success-foregroundSuccess text#ffffff#0f172a
--warningWarning#f59e0b#fbbf24
--warning-foregroundWarning text#ffffff#0f172a
--infoInfo#3b82f6#60a5fa
--info-foregroundInfo text#ffffff#0f172a
--borderGeneric border#e5e7eb#334155
--inputForm border#e5e7eb#334155
--ringFocus ring#3b82f6#60a5fa

Typography

VariableDefault
--font-sanssystem-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

VariableDefaultNote
--radius0.5remRadius baseline — the one businesses tweak most
--shadow-sm0 1px 3px rgb(0 0 0 / 8%)Small shadow
--shadow-md0 4px 12px rgb(0 0 0 / 12%)Medium shadow
--shadow-lg0 10px 30px rgb(0 0 0 / 16%)Large shadow
--scrimlinear-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-toplinear-gradient(rgb(0 0 0 / 45%), transparent)Top cover scrim (player top bar / header-over-image scenarios)

Form controls

VariableDefaultNotes
--control-height2remUnified form-control height (SwInput / SwSelect / SwSegmented align by default; override to scale every control site-wide)
--control-font-size0.875remUnified form-control font size (SwInput bumps back to 1rem on touch devices to avoid the iOS focus-zoom on <16px inputs)

Layout

VariableDefaultNote
--nav-height60px (mobile 56px)Top nav height
--container-padding24px (mobile 16px; ≥1280 48px, ≥1600 96px)Container horizontal padding — wide-screen margins come from this, not from --content-max-width
--content-max-width1920pxMax 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-4 etc.), fontSize / fontWeight / lineHeight, color scales (--gray-50–--gray-900). With Tailwind use p-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)

VariableDefaultDescription
--sw-modal-overlay-bgrgb(0 0 0 / 45%)Overlay color (fixed, theme-invariant)
--sw-modal-bgvar(--background)Dialog background
--sw-modal-bordervar(--border)Dialog border
--sw-modal-shadowvar(--shadow-lg)Dialog shadow
--sw-modal-radius0.75remDialog radius
--sw-modal-width28rem (inline fallback)Dialog width (actual min(90vw, …))
--sw-modal-title-colorvar(--foreground)Title color
--sw-modal-desc-colorvar(--muted-foreground)Description color
--sw-modal-text-colorvar(--foreground)Body color
--sw-modal-close-colorvar(--muted-foreground)Close ✕ color
--sw-modal-close-bgtransparent✕ background (solid color + radius: 50% → round filled button)
--sw-modal-close-radius6px✕ corner radius (set 50% for a circle)
--sw-modal-close-size1.75rem✕ square size
--sw-modal-close-hover-bgvar(--muted)✕ hover background
--sw-modal-close-hover-colorvar(--foreground)✕ hover color
--sw-modal-footer-bordervar(--border)Footer divider
--sw-modal-btn-cancel-bgvar(--background)Cancel button background
--sw-modal-btn-cancel-bordervar(--border)Cancel button border
--sw-modal-btn-cancel-colorvar(--foreground)Cancel button text
--sw-modal-btn-cancel-hover-bgvar(--muted)Cancel hover background
--sw-modal-btn-confirm-bgvar(--primary)Confirm button background (hover via filter: brightness)
--sw-modal-btn-confirm-colorvar(--primary-foreground)Confirm button text

--sw-toast-* (SwToast / useToast)

VariableDefaultDescription
--sw-toast-bgcolor-mix(in srgb, var(--background) 92%, transparent)Toast background (frosted translucent)
--sw-toast-colorvar(--foreground)Text color
--sw-toast-bordervar(--border)Border
--sw-toast-shadowvar(--shadow-md)Shadow
--sw-toast-close-colorvar(--muted-foreground)Close ✕ color
--sw-toast-close-hover-bgvar(--muted)✕ hover background
--sw-toast-close-hover-colorvar(--foreground)✕ hover color
--sw-toast-icon-successvar(--success)✓ icon color
--sw-toast-icon-errorvar(--destructive)✕ icon color
--sw-toast-icon-warningvar(--warning)! icon color
--sw-toast-icon-infovar(--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 / --foregroundbg-background / text-foreground
--card / --card-foregroundbg-card / text-card-foreground
--popover / --popover-foregroundbg-popover / text-popover-foreground
--primary / --primary-foregroundbg-primary / text-primary-foreground / border-primary
--secondary / --accent / --muted (with their foregrounds)bg-secondary / bg-accent / bg-muted … (same pattern)
--destructive / --success / --warning / --infobg-destructive / text-success … (same pattern)
--border / --input / --ringborder-border / border-input / ring-ring
--radius (derives sm/md/lg/xl)rounded-sm / rounded-md / rounded-lg / rounded-xl
--font-sans / --font-monofont-sans / font-mono
--shadow-sm / --shadow-md / --shadow-lgshadow-sm / shadow-md / shadow-lg
Breakpoints 768px / 1024pxsm: / md: prefixes

Radius note: native CSS has only the single var(--radius); rounded-sm/md/lg/xl are derived by the preset from --radius via calc() (override --radius to 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 token var(--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):

BreakpointRange
Mobile< 768px
Tablet768px – 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:

ClassEffect
.sw-containerResponsive container: max-width: var(--content-max-width) + centered + horizontal var(--container-padding)
.sw-show-mobileShown only < 768px (hidden otherwise)
.sw-hide-mobileHidden < 768px
.sw-hide-tabletHidden 768px – 1023px
.sw-hide-desktopHidden ≥ 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-width sets "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-padding sets "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-width leaves 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.