Documentation Index
Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt
Use this file to discover all available pages before exploring further.
Vertz provides a built-in styling system based on design tokens. No CSS files, no class name collisions, no build-step CSS tooling. You write styles in TypeScript and get scoped class names at runtime.
css() — scoped styles
Create a group of scoped styles with css() and the token helper:
import { css, token } from '@vertz/ui';
const styles = css({
card: {
backgroundColor: token.color.card,
borderRadius: token.radius.lg,
padding: token.spacing[4],
borderWidth: '1px',
borderColor: token.color.border,
},
title: {
fontSize: token.font.size.lg,
fontWeight: token.font.weight.bold,
color: token.color.foreground,
},
meta: {
fontSize: token.font.size.sm,
color: token.color['muted-foreground'],
},
});
export function TaskCard({ task }: TaskCardProps) {
return (
<div className={styles.card}>
<h3 className={styles.title}>{task.title}</h3>
<span className={styles.meta}>Created {task.createdAt}</span>
</div>
);
}
Each key in the object becomes a scoped class name. CSS property names use camelCase (backgroundColor, borderRadius). The token helper resolves to CSS custom properties from your theme.
Token helper
The token namespace is typed against your active theme. Use dot notation for camelCase token names and bracket notation for any token name that contains a hyphen or a shade number.
| Expression | CSS output |
|---|
token.color.card | var(--color-card) |
token.color.foreground | var(--color-foreground) |
token.color['muted-foreground'] | var(--color-muted-foreground) |
token.color.primary[600] | var(--color-primary-600) |
token.spacing[4] | var(--spacing-4) |
token.font.size.lg | var(--font-size-lg) |
token.font.weight.bold | var(--font-weight-bold) |
token.radius.lg | var(--radius-lg) |
Raw CSS keywords like 'flex', 'center', '100%', 'inherit', or 'transparent' are passed through literally.
const styles = css({
row: {
display: 'flex',
alignItems: 'center',
gap: token.spacing[2],
},
});
The full token set comes from your theme (@vertz/theme-shadcn or a custom theme defined with defineTheme()).
Shade color tokens
Color palettes support a full shade scale beyond the semantic tokens. Use bracket notation for the numeric shade:
const styles = css({
heading: { color: token.color.gray[900] },
subtitle: { color: token.color.zinc[500] },
card: {
backgroundColor: token.color.gray[50],
borderWidth: '1px',
borderColor: token.color.gray[200],
},
accent: {
backgroundColor: token.color.blue[600],
color: 'white',
},
warning: {
backgroundColor: token.color.amber[100],
color: token.color.amber[800],
borderWidth: '1px',
borderColor: token.color.amber[300],
},
});
Available namespaces — these are the color namespaces that support shade notation:
| Namespace | Example | CSS output |
|---|
primary | token.color.primary[600] | background-color: var(--color-primary-600) |
secondary | token.color.secondary[400] | color: var(--color-secondary-400) |
accent | token.color.accent[100] | background-color: var(--color-accent-100) |
gray | token.color.gray[500] | color: var(--color-gray-500) |
destructive | token.color.destructive[600] | background-color: var(--color-destructive-600) |
danger | token.color.danger[500] | color: var(--color-danger-500) |
success | token.color.success[100] | background-color: var(--color-success-100) |
warning | token.color.warning[300] | border-color: var(--color-warning-300) |
info | token.color.info[50] | background-color: var(--color-info-50) |
muted | token.color.muted[600] | color: var(--color-muted-600) |
surface | token.color.surface[100] | background-color: var(--color-surface-100) |
Shade scale — each namespace supports shades from 50 (lightest) to 950 (darkest):
50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
The actual color values are defined by your theme. When using palettes from @vertz/ui/css, the Tailwind v4 oklch color palettes are available (slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose).
Using palettes in your theme:
import { defineTheme } from '@vertz/ui';
import { palettes } from '@vertz/ui/css';
const theme = defineTheme({
colors: {
primary: palettes.blue, // all shades 50-950
danger: palettes.red,
success: palettes.emerald,
gray: palettes.zinc,
},
});
Pseudo-state selectors
Use nested &: selectors to apply styles on pseudo-states:
const styles = css({
link: {
color: token.color.primary,
textDecoration: 'none',
'&:hover': {
color: token.color.primary[700],
textDecoration: 'underline',
},
},
input: {
borderWidth: '1px',
borderColor: token.color.border,
borderRadius: token.radius.md,
paddingInline: token.spacing[3],
paddingBlock: token.spacing[2],
'&:focus': {
borderColor: token.color.primary,
boxShadow: '0 0 0 2px var(--color-ring)',
},
'&:disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
},
});
variants() — parameterized styles
Use variants() for components with multiple visual states:
import { variants, token } from '@vertz/ui';
const button = variants({
base: {
display: 'inline-flex',
alignItems: 'center',
borderRadius: token.radius.md,
fontWeight: token.font.weight.medium,
transition: 'all 150ms ease',
},
variants: {
intent: {
primary: {
backgroundColor: token.color.primary,
color: token.color['primary-foreground'],
},
secondary: {
backgroundColor: token.color.secondary,
color: token.color['secondary-foreground'],
},
ghost: {
backgroundColor: 'transparent',
color: token.color.foreground,
},
danger: {
backgroundColor: token.color.destructive,
color: 'white',
},
},
size: {
sm: {
fontSize: token.font.size.xs,
paddingInline: token.spacing[3],
paddingBlock: token.spacing[1],
},
md: {
fontSize: token.font.size.sm,
paddingInline: token.spacing[4],
paddingBlock: token.spacing[2],
},
lg: {
fontSize: token.font.size.base,
paddingInline: token.spacing[6],
paddingBlock: token.spacing[3],
},
},
},
defaultVariants: { intent: 'primary', size: 'md' },
});
Use it in JSX by calling the function with variant values:
<button className={button({ intent: 'primary', size: 'lg' })}>
Save
</button>
<button className={button({ intent: 'ghost', size: 'sm' })}>
Cancel
</button>
{/* Uses defaultVariants when omitted */}
<button className={button()}>
Default
</button>
Reactive variants
Variant values can be reactive — the class name updates automatically when the signal changes:
let isActive = false;
return (
<button
className={button({ intent: isActive ? 'primary' : 'ghost' })}
onClick={() => {
isActive = !isActive;
}}
>
Toggle
</button>
);
When to use css() vs inline styles
Use css() for:
- Layout and spacing (
display: 'flex', padding: token.spacing[4], gap: token.spacing[2])
- Sizing (
width: '100%', height: '100vh') and aspect ratios (aspectRatio: '16 / 9')
- Positioning (
position: 'absolute', top: 0, left: token.spacing[4], inset: 0)
- Images (
objectFit: 'cover', objectFit: 'contain')
- Typography scales (
fontSize: token.font.size.lg, fontWeight: token.font.weight.semibold)
- Borders and radius (
borderWidth: '1px', borderColor: token.color.border, borderRadius: token.radius.lg)
- Colors from the theme/shade system (
backgroundColor: token.color.primary[600], color: token.color.gray[500])
- Pseudo-states (
&:hover, &:focus, &:disabled)
Use inline style for:
- Truly dynamic values computed at runtime (
transform, translate)
- Syntax highlight colors or external color values
- Complex CSS that has no token equivalent (gradients,
clamp(), backdrop-filter)
- Custom shadows beyond the shadow scale
- CSS custom property references (
var(--my-custom-prop))
{
/* css() -- theme-aware, scoped, pseudo-state support */
}
const styles = css({
card: {
backgroundColor: token.color.card,
borderRadius: token.radius.lg,
padding: token.spacing[6],
boxShadow: 'var(--shadow-md)',
},
title: {
fontSize: token.font.size.xl,
fontWeight: token.font.weight.bold,
color: token.color.foreground,
},
});
{
/* inline style -- dynamic runtime value */
}
<div style={{ transform: `translateX(${offset}px)` }}>Sliding content</div>;
{
/* inline style -- complex CSS without token equivalent */
}
<div style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
Gradient background
</div>;
Theme
Vertz uses CSS custom properties for theming. The @vertz/theme-shadcn package provides a shadcn/ui-inspired token set with light and dark mode support.
import { defineTheme, ThemeProvider } from '@vertz/ui';
import { shadcnTheme } from '@vertz/theme-shadcn';
// Use the built-in theme
ThemeProvider({ theme: shadcnTheme }, () => <App />);
Custom themes
Define your own theme with defineTheme():
import { defineTheme } from '@vertz/ui';
const myTheme = defineTheme({
colors: {
background: '#ffffff',
foreground: '#0f0f12',
primary: '#3b82f6',
// ... other tokens
},
dark: {
background: '#0f0f12',
foreground: '#fafafa',
primary: '#60a5fa',
},
});
Global styles
For reset styles or global base rules, use globalCss():
import { globalCss, token } from '@vertz/ui';
globalCss({
'*': {
boxSizing: 'border-box',
margin: 0,
padding: 0,
},
body: {
backgroundColor: token.color.background,
color: token.color.foreground,
fontFamily: 'var(--font-sans)',
},
});
Global styles are injected once and aren’t scoped — use sparingly.
Fonts
Declare font families with font() and compile them into @font-face CSS, custom properties (--font-<key>), and preload tags with compileFonts(). Only woff2 format is supported.
import { font, compileFonts } from '@vertz/ui/css';
const sans = font('DM Sans', {
weight: '100..1000',
src: '/fonts/dm-sans.woff2',
fallback: ['system-ui', 'sans-serif'],
});
const mono = font('JetBrains Mono', {
weight: '100..800',
src: '/fonts/jb-mono.woff2',
fallback: ['monospace'],
});
const compiled = compileFonts({ sans, mono });
// compiled.fontFaceCss — @font-face declarations
// compiled.cssVarsCss — :root { --font-sans: ...; --font-mono: ...; }
// compiled.cssVarLines — individual lines for merging into an existing :root
// compiled.preloadTags — <link rel="preload" ...> HTML tags
Reference the generated CSS vars in components:
function CodeBlock({ code }: { code: string }) {
return (
<pre style={{ fontFamily: 'var(--font-mono)' }}>
<code>{code}</code>
</pre>
);
}
Multiple font files
Pass an array of src entries for variants like normal + italic:
const sans = font('DM Sans', {
weight: '100..1000',
src: [
{ path: '/fonts/dm-sans.woff2', weight: '100..1000', style: 'normal' },
{ path: '/fonts/dm-sans-italic.woff2', weight: '100..1000', style: 'italic' },
],
fallback: ['system-ui', 'sans-serif'],
});
Each entry generates a separate @font-face block. Only the first file is preloaded.
System fonts (no src)
Omit src to generate a CSS custom property without an @font-face block:
const system = font('system-ui', { weight: '400' });
// compileFonts({ system }) → --font-system: 'system-ui'; (no @font-face)
Google Fonts
Use googleFont() to declare a Google Font by name. The framework automatically fetches the .woff2 file at dev/build time, caches it locally, and serves it self-hosted. No manual downloads needed.
import { googleFont } from '@vertz/ui/css';
import { configureTheme } from '@vertz/theme-shadcn';
import { registerTheme } from '@vertz/ui';
const sans = googleFont('Inter', {
weight: '100..900',
subsets: ['latin'],
});
const mono = googleFont('JetBrains Mono', {
weight: '100..800',
subsets: ['latin'],
});
const config = configureTheme({
palette: 'zinc',
fonts: { sans, mono },
});
registerTheme(config);
That’s the complete setup. The dev server handles everything else:
- Fetches the font from Google Fonts CSS2 API on first startup
- Downloads the
.woff2 file to .vertz/fonts/ (cached across restarts)
- Generates
@font-face CSS with the local file path
- Computes zero-CLS fallback metrics automatically
- Injects
<link rel="preload"> tags in SSR
googleFont() options:
| Option | Type | Default | Description |
|---|
weight | string | number | number[] | (required) | Weight range ('100..900'), single weight (400), or multiple ([400, 700]) |
style | 'normal' | 'italic' | ('normal' | 'italic')[] | 'normal' | Font style(s) |
display | 'auto' | 'block' | 'swap' | 'fallback' | 'optional' | 'swap' | font-display strategy |
subsets | string[] | ['latin'] | Character subsets |
fallback | string[] | auto-detected | Fallback font stack |
adjustFontFallback | boolean | true | Enable zero-CLS metric-adjusted fallback |
Static font with specific weights:
const heading = googleFont('Playfair Display', {
weight: [400, 700],
style: ['normal', 'italic'],
subsets: ['latin'],
});
Using font() alongside googleFont():
font() and googleFont() return the same FontDescriptor type and can be mixed freely in themes:
import { font, googleFont } from '@vertz/ui/css';
const sans = googleFont('Inter', { weight: '100..900' });
const brand = font('Custom Brand', {
weight: '400..700',
src: '/fonts/brand.woff2',
fallback: ['sans-serif'],
});
configureTheme({ fonts: { sans, brand } });
Google Fonts are all open-source licensed (SIL OFL or Apache 2.0). Self-hosting is explicitly
permitted.
Offline and CI caching
Font files are cached in .vertz/fonts/ and persist across dev server restarts. For CI environments without network access, cache this directory:
# GitHub Actions example
- uses: actions/cache@v4
with:
path: .vertz/fonts
key: vertz-fonts-${{ hashFiles('src/styles/theme.ts') }}
If the cache is empty and no network is available, the dev server logs an error and falls back to system fonts.