Theming and Styling
import { ButtonBase, CardBase, resolveTokens } from "@lastshotlabs/snapshot/ui";
const snapshotCss = resolveTokens({ flavor: "neutral", overrides: { colors: { primary: "#3b82f6", accent: "#8b5cf6", }, radius: "md", font: { sans: "Inter" }, },});
// Inject <style>{snapshotCss}</style> once near the app root.<ButtonBase label="Themed button" variant="default" />
// Customize individual components with slots<CardBase title="Custom Card" slots={{ root: { className: "shadow-lg", style: { borderRadius: "1rem" } }, title: { style: { fontSize: "1.5rem", fontWeight: "bold" } }, }}> Content</CardBase>Design tokens
Section titled “Design tokens”Tokens are resolved from code with resolveTokens() and compiled to CSS custom properties on :root and .dark.
Colors
Section titled “Colors”resolveTokens({ overrides: { colors: { primary: "#3b82f6", // --sn-color-primary accent: "#8b5cf6", // --sn-color-accent background: "#ffffff", // --sn-color-background surface: "#f8fafc", // --sn-color-surface muted: "#94a3b8", // --sn-color-muted border: "#e2e8f0", // --sn-color-border success: "#10b981", // --sn-color-success warning: "#f59e0b", // --sn-color-warning error: "#ef4444", // --sn-color-error }, },});Each color automatically generates a foreground pair (--sn-color-primary-foreground) computed for contrast.
Spacing
Section titled “Spacing”Spacing tokens are used by gap, padding, and spacing props throughout all components:
| Token | CSS Variable | Default |
|---|---|---|
2xs | --sn-spacing-2xs | 0.25rem |
xs | --sn-spacing-xs | 0.5rem |
sm | --sn-spacing-sm | 0.75rem |
md | --sn-spacing-md | 1rem |
lg | --sn-spacing-lg | 1.5rem |
xl | --sn-spacing-xl | 2rem |
2xl | --sn-spacing-2xl | 3rem |
Border radius
Section titled “Border radius”resolveTokens({ overrides: { radius: "md", },});resolveTokens({ overrides: { font: { sans: "Inter", // --sn-font-sans mono: "JetBrains Mono", // --sn-font-mono display: { family: "Outfit", weights: [400, 700] }, // --sn-font-display }, },});Google Fonts are auto-loaded when recognized (Inter, Roboto, Open Sans, Poppins, Montserrat, etc.).
Every standalone component accepts a slots prop for targeting sub-elements.
Basic slot usage
Section titled “Basic slot usage”<ButtonBase label="Save" variant="default" slots={{ root: { className: "my-button", style: { minWidth: "120px" } }, label: { style: { fontWeight: "bold" } }, icon: { style: { color: "green" } }, }}/>Common slot names
Section titled “Common slot names”Most components share these patterns:
| Slot | Target |
|---|---|
root | Outermost wrapper element |
label | Text label element |
icon | Icon element |
title | Title text |
subtitle | Subtitle text |
content | Main content area |
header | Header region |
footer | Footer region |
Each component documents its available slot names in its props interface.
Slot merge order
Section titled “Slot merge order”When multiple style sources apply, they merge in this order (later wins):
- Implementation base styles (component defaults)
- Component surface config (from
surfaceConfig) - Item surface config (from
slots) - Active states (hover, focus, open, etc.)
Slot className and style
Section titled “Slot className and style”Both className and style can be set on any slot:
<DataTableBase columns={columns} rows={data} slots={{ root: { className: "my-table" }, header: { style: { backgroundColor: "#f0f0f0", fontWeight: "bold" } }, row: { style: { borderBottom: "1px solid #e0e0e0" } }, }}/>Dark mode
Section titled “Dark mode”Toggle with useTheme
Section titled “Toggle with useTheme”const { theme, set, toggle } = snap.useTheme();
<ButtonBase label={theme === "dark" ? "Light mode" : "Dark mode"} icon={theme === "dark" ? "sun" : "moon"} variant="ghost" onClick={toggle}/>useTheme adds/removes the .dark class on <html>. All token CSS variables have dark-mode overrides that activate automatically.
Persisted dark mode with system preference
Section titled “Persisted dark mode with system preference”Persist the user’s choice to localStorage and respect the system preference on first visit:
import { useEffect, useState } from "react";
function ThemeProvider({ children }: { children: React.ReactNode }) { const { theme, set } = snap.useTheme();
// On mount: restore from localStorage, or fall back to system preference useEffect(() => { const stored = localStorage.getItem("sn-theme"); if (stored === "dark" || stored === "light") { set(stored); } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { set("dark"); } }, []);
// Persist changes const setAndPersist = (mode: "light" | "dark") => { set(mode); localStorage.setItem("sn-theme", mode); };
return <>{children}</>;}
// A toggle button that persists the choicefunction ThemeToggle() { const { theme, set } = snap.useTheme();
const toggle = () => { const next = theme === "dark" ? "light" : "dark"; set(next); localStorage.setItem("sn-theme", next); };
return ( <ButtonBase label={theme === "dark" ? "Light mode" : "Dark mode"} icon={theme === "dark" ? "sun" : "moon"} variant="ghost" onClick={toggle} /> );}To also listen for system preference changes (e.g. the user changes OS settings while your app is open):
useEffect(() => { const mql = window.matchMedia("(prefers-color-scheme: dark)"); const handler = (e: MediaQueryListEvent) => { // Only auto-switch if no manual preference is stored if (!localStorage.getItem("sn-theme")) { set(e.matches ? "dark" : "light"); } }; mql.addEventListener("change", handler); return () => mql.removeEventListener("change", handler);}, []);Dark-mode token overrides
Section titled “Dark-mode token overrides”Colors set in overrides.colors automatically derive dark variants using OKLCH color space. You can also set explicit dark overrides:
resolveTokens({ overrides: { colors: { background: "#ffffff", card: "#f8fafc", }, darkColors: { background: "#0f172a", card: "#1e293b", }, },});Applying styles to components
Section titled “Applying styles to components”className and style
Section titled “className and style”Every component accepts className and style on the root element:
<CardBase className="dashboard-card" style={{ maxWidth: "400px", margin: "0 auto" }} title="Revenue"> <StatCardBase value="$48K" label="Total" /></CardBase>Responsive patterns
Section titled “Responsive patterns”Use CSS media queries via className or inline styles:
<GridBase columns="repeat(auto-fill, minmax(300px, 1fr))" gap="md" className="responsive-grid"> {items.map((item) => <CardBase key={item.id} title={item.name} />)}</GridBase>Next steps
Section titled “Next steps”- Layout and Navigation — themed app shells
- Forms and Validation — styled form components
- Component Library reference — all 114 components