GUI Styling & UX Guide
The official style guide for SveltyCMS native components, ensuring a consistent, accessible, and high-quality user experience.
On this page
SveltyCMS admin UI is built on native Svelte 5 components, Tailwind CSS v4, and the admin theme token system. Prefer composition of shell + primitives over one-off page chrome.
Mandatory shells
| Component | Use for |
|---|---|
AdminPageShell |
Every (app) page root — title, back link, spacing, admin-theme-container |
AdminCard |
Content blocks — semantic card surface, density radii, theme shadows |
PageTitle |
Only via AdminPageShell (do not invent inline <h1> headers) |
<AdminPageShell title="Example" icon="mdi:cog" description="…">
<AdminCard class="p-4">…</AdminCard>
</AdminPageShell>
Native primitives
Always prefer:
<Button>,<Badge>,<Input>,<Select>,<Textarea>,<Toggle>,<Checkbox>,<Modal>
Never mix component CSS classes (class="badge" on a <Button>, class="btn" on a <Badge>).
Semantic admin surfaces
Use role tokens from src/utilities.css instead of paired light/dark surface classes for admin chrome:
| Token | Role |
|---|---|
--admin-bg-page |
Page canvas |
--admin-bg-card |
Elevated card (dark mode sits above page) |
--admin-bg-sidebar |
Sidebar |
--admin-border-default / --admin-border-subtle |
Borders |
--admin-text-body / --admin-text-muted |
Text |
Shadows and border width follow card variant (flat | bordered | elevated) via AdminTheme.cardShadow / cardBorder. Do not hardcode hover:shadow-lg on every card — that breaks flat themes.
Theme/shorthand palette import maps surface into full shade scales and rebinds --admin-bg-* in generated customCss (theme-preset-mapper.ts). The generated CSS is scoped to :root, .admin-theme-container, [data-admin-theme] so a theme change repaints every element — utilities, --admin-* roles, scrollbars, and portalled overlays — with no per-component JS.
Motion & Transitions
All shared motion lives in src/utils/admin-transitions.ts and is reduced-motion safe (drops to 0ms when prefers-reduced-motion: reduce or the theme’s reducedMotion feature is enabled).
| Transition | When to use |
|---|---|
adminPage |
Page entry (240ms fade + subtle rise) — applied once by (app)/+layout.svelte, keyed on page.url.pathname |
adminFade |
Modal / section reveals (200ms fade) |
adminStagger |
Card grids / list rows — pass index for a cascade |
adminSlide |
Drawers / sidebars — signed distance for direction |
View Transitions: on browsers that support document.startViewTransition, (app)/+layout.svelte wraps SPA navigations in a native cross-fade via SvelteKit’s onNavigate. adminPage remains the fallback (and the only path under reduced motion) — the two never double-animate.
Try the Motion & transitions section on Design System → Live Preview (/config/design-system?tab=preview) for interactive demos of every transition and the --admin-motion-* timing tokens.
<AdminCard in:adminStagger={{ index: i }}>…</AdminCard>
Rules:
- Do not add per-page
in:fly/in:scaleone-offs — the layout owns the single page transition so every route (dashboard → user → system settings) moves identically. - Hover / active micro-interactions: 150–200ms
transition-allwithease-out. - Animate only
transform+opacity(neverwidth,height,top,gap) for jank-free motion. - Query-parameter navigation (
?edit,?create, filters) must not replay the page transition — that is why the layout keys onpathnameonly.
Color conventions
- Neutral aliases:
gray,slate,neutral,zinc,stoneare aliased to the surface ramp in@theme(--color-gray-500: var(--color-surface-500)), so every neutral utility follows the active theme. Do not reintroduce independent neutral ramps. - Alpha on tokens: use
color-mix(in srgb, var(--color-primary-500) 30%, transparent). Never writergb(var(--color-primary-500) / 0.3)— tokens resolve tooklch()/hex at runtime, which is invalid insidergb()and the declaration is silently dropped (this left scrollbars and drag outlines unstyled in 8 files before the fix). - Hardcoded
bg-white/text-blackon themeable surfaces breaks dark mode — usesurfacetokens or paireddark:variants. Legitimate exceptions: image overlays, QR cards (scanning needs a white canvas), always-dark error pages.
Status-shade contract
One hue, one job. Every semantic ramp (error, success, warning, tertiary, primary, secondary, surface) uses the same shade step for the same role. Reaching for adjacent shades is what produced “too many different reds” across the admin. The drift guard scripts/consolidate-shades.mjs --check runs inside bun run check and exits 1 on any violation; run the same script without --check to auto-repair (idempotent, re-run after any big UI sweep).
| Role | Light | Dark |
|---|---|---|
| Solid fill / icon chip | {hue}-500 |
{hue}-500 |
| Accent text | {hue}-500 |
{hue}-400 |
| Emphasis text (headings, bold counts on washes) | {hue}-600 |
{hue}-400 |
| Subtle wash (panel / chip / alert background) | {hue}-500/10 |
{hue}-900/20 |
| Active / toggle wash | {hue}-500/20 |
{hue}-500/20 |
| Soft border (status alert outline) | {hue}-500/30 |
{hue}-500/40 |
| Accent border | {hue}-500 |
{hue}-400 |
| Focus ring | {hue}-500/20–/40 |
same |
Forbidden drift (the consolidation removes these)
- ❌
bg-{hue}-50/bg-{hue}-100— usebg-{hue}-500/10 - ❌
bg-{hue}-500/5//15— use/10 - ❌
text-{hue}-700/-800in light mode — use-600 - ❌
dark:text-{hue}-200/-300— use-400 - ❌
border-{hue}-200/-300— use-500/30;border-{hue}-700/-800— use-500/40 - ❌ Legacy Tailwind hues in admin chrome:
red→error,rose→error,blue→tertiary,green/emerald→success,amber/orange/yellow→warning
Deliberate exceptions
- Filled
warningsurfaces use dark text ({hue}-900/-950), not white — white-on-amber fails WCAG AA (restart-required banner, highlighted search terms). - Light text on dark status surfaces (
text-{hue}-50/-100onbg-{hue}-900/*) stays paired — error toasts, dark modal tinting. - Decorative brand hues (
indigo,purple,pink,violet,teal,cyan) are allowed for seasonal effects, preset swatches, FloatingNav spokes and non-theme data viz — never for status semantics. {hue}-900/-950solid fills and{hue}-900/10remain valid dark-surface accents;{hue}-400solid is allowed for small status dots on dark surfaces.src/app.csspreset recipes are owner-tuned: thepreset-tonal-*/preset-outlined-*/preset-ghost-*/preset-soft-surfaceutilities intentionally usetext-{hue}-700anddark:text-{hue}-200/-300emphasis steps for contrast on washes.src/app.cssis exempted inconsolidate-shades.mjs(EXCLUDED) so the guard never rewrites it — component code must still follow the contract.
Canonical recipes live in src/app.css (preset-tonal-*, preset-outlined-*, preset-ghost-*) and src/components/ui/status-badge.svelte. The --admin-* token layer in src/utilities.css stays the single source for chrome surfaces.
Responsive behavior
Every (app) route must work at mobile (<768px), tablet (768–1023px), and desktop (≥1024px):
- CSS layout: Tailwind
sm:/md:/lg:variants. - JS logic: the
screenstore (screen.isMobile/isTablet/isDesktop). - Wide tables / panels go in an
overflow-x-autowrapper. - Never rely on
hover:alone for primary actions (touch devices), and keep touch targets ≥ 40px.
RTL / LTR
SveltyCMS is fully localized (Paraglide) and must render correctly in both directions. Use logical properties so one class set works for LTR and RTL:
- Margins/padding:
ms-/me-/ps-/pe-(neverml-/mr-/pl-/pr-). - Positioning:
start-/end-/inset-s-/inset-e-(neverleft-/right-). - Alignment:
text-start/text-end(nevertext-left/text-right). - Corners/borders:
rounded-s-/rounded-e-/border-s-/border-e-(neverrounded-l-/rounded-r-/border-l-/border-r-). - Transforms (
-translate-x-*) are physical — for directional icons (chevrons), mirror them withrtl:-scale-x-[-1].
Exception: image editors / canvas overlays use physical coordinates (left-1/2, -translate-x-1/2) because the canvas itself is never mirrored.
Light & dark mode
- Semantic
--admin-*roles flip automatically viahtml.dark— prefer them over paireddark:surface classes on chrome. dark:variants are allowed for one-off overrides (gradients, accent chips), not for page chrome.- Status colors come from theme ramps (
error,warning,success) — do not hardcode Tailwind’sred-*/amber-*/emerald-*for state.
Design System workspace
Canonical route: /config/design-system
| Tab | Purpose |
|---|---|
?tab=overrides |
Per-user density, card style, a11y, layout — linked from /user |
?tab=preview |
Live component catalog + token readout |
?tab=presets |
Palette studio (seed colors → runtime CSS) + Theme JSON import |
?tab=themes … |
Admin workspace themes, layout density, features, advanced |
Palette studio writes a marked customCss block via buildPaletteCssFromSeeds / mergePaletteCssIntoCustomCss — do not invent a second token system.
See Design System & Admin Theme Settings.
Accessibility
- Interactive controls need accessible names
- Prefer logical Tailwind properties (
ps-,pe-) for RTL - Respect theme Reduced Motion and High Contrast
- Keyboard focus is visible in both modes (
focus-visiblerings use theme tokens, neveroutline: none)
What not to do
- ❌ Parallel global token systems that collide with
surface-*palettes - ❌ Hand-rolled page shells instead of
AdminPageShell - ❌ Soft-skipping E2E because lists are empty — seed fixtures instead
- ❌
rgb(var(--color-*) / a)on palette tokens — silently invalid; usecolor-mix() - ❌ Per-page transition one-offs — use
adminPage/adminStaggerfrom@utils/admin-transitions - ❌ Hardcoded
bg-white/text-black/ Tailwindgray-*-as-semantics on themeable surfaces - ❌ Shade drift (see Status-shade contract):
bg-{hue}-50washes,text-{hue}-700emphasis,border-{hue}-200soft borders, legacyred/blue/greenclasses