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 has strong conventions. None of these are enforced by the compiler or runtime — they’re patterns that keep your code consistent, readable, and predictable for both humans and LLMs.
Component signatures
Destructure props in the parameter
// Preferred
export function TaskCard({ task, onClick }: TaskCardProps) {
return <div onClick={() => onClick(task.id)}>{task.title}</div>;
}
// Avoid
export function TaskCard(props: TaskCardProps) {
const { task, onClick } = props;
return <div onClick={() => onClick(task.id)}>{task.title}</div>;
}
Props interface naming
Name the interface ComponentNameProps. Use the on prefix for callback props.
interface TaskCardProps {
task: Task;
onClick: (id: string) => void;
onDelete?: (id: string) => void;
}
Don’t annotate return types
Let TypeScript infer component return types. The JSX factory maps tag names to specific element types — an explicit : HTMLElement annotation is a lossy upcast.
// Preferred — TypeScript infers HTMLFormElement
export function TaskForm({ onSuccess }: TaskFormProps) {
return <form>...</form>;
}
// Avoid — loses type specificity
export function TaskForm({ onSuccess }: TaskFormProps): HTMLElement {
return <form>...</form>;
}
Reactivity
let for local state
Use let for local state. The compiler transforms it into a signal automatically.
let count = 0;
let isOpen = false;
return (
<button
onClick={() => {
count++;
isOpen = true;
}}
>
{count}
</button>
);
const for derived values
Use const for values derived from state. The compiler wraps it in computed() automatically.
let count = 0;
const doubled = count * 2;
const label = count === 0 ? 'empty' : `${count} items`;
JSX
Fully declarative
All UI code must be declarative. No appendChild, innerHTML, textContent assignment, setAttribute, or document.createElement.
// Preferred
return <div className={styles.panel}>{title}</div>;
// Avoid
const el = document.createElement('div');
el.textContent = title;
el.className = styles.panel;
If you can’t express something declaratively, that’s a framework gap — not a reason to use imperative DOM APIs.
Use JSX for components, not function calls
// Preferred
<TaskCard task={task} onClick={handleClick} />;
// Avoid
{
TaskCard({ task, onClick: handleClick });
}
Conditionals with && and ternary
{
isLoading && <div>Loading...</div>;
}
{
error ? <ErrorView error={error} /> : <Content data={data} />;
}
Lists with .map() and key
{
tasks.map((task) => <TaskCard key={task.id} task={task} />);
}
Styling
css() for scoped styles
import { css, token } from '@vertz/ui';
const styles = css({
card: {
backgroundColor: token.color.card,
borderRadius: token.radius.lg,
padding: token.spacing[4],
},
title: {
fontSize: token.font.size.lg,
fontWeight: token.font.weight.bold,
},
});
variants() for parameterized styles
import { variants, token } from '@vertz/ui';
const button = variants({
base: {
display: 'inline-flex',
borderRadius: token.radius.md,
fontWeight: token.font.weight.medium,
},
variants: {
intent: {
primary: { backgroundColor: token.color.primary, color: 'white' },
ghost: { backgroundColor: 'transparent', color: token.color.foreground },
},
size: {
sm: { fontSize: token.font.size.xs, paddingInline: token.spacing[3] },
md: { fontSize: token.font.size.sm, paddingInline: token.spacing[4] },
},
},
defaultVariants: { intent: 'primary', size: 'md' },
});
Inline style only for dynamic values
Use css() tokens for anything theme-related. Reserve inline styles for one-off layout values like margins, transforms, or dynamic positioning.
// Theme values — use css() tokens
const styles = css({
panel: {
backgroundColor: token.color.card,
borderRadius: token.radius.lg,
padding: token.spacing[6],
},
});
// Dynamic one-off layout — inline style is fine
<div className={styles.panel} style={{ marginTop: `${offset}px` }}>
{content}
</div>;
Data fetching
query() is auto-disposed
Don’t manually manage query lifecycle — queries auto-register cleanup with the component scope. When the component unmounts, the query stops reactive effects and timers automatically.
const tasks = query(api.tasks.list());
// Just use it — no cleanup needed
return (
<ul>
{tasks.data?.items.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
Access query properties directly
Query properties like .data, .loading, and .error work directly in both JSX and event handlers — no unwrapping needed.
const tasks = query(api.tasks.list());
return (
<div>
{tasks.loading && <span>Loading...</span>}
{tasks.error && <span>Failed to load</span>}
{tasks.data?.items.map((task) => (
<li key={task.id}>{task.title}</li>
))}
<button onClick={() => console.log(tasks.data?.items.length)}>Log count</button>
</div>
);
Context
Create a convenience hook
Always create a typed use* accessor that throws on missing provider.
export const SettingsContext = createContext<SettingsContextValue>();
export function useSettings(): SettingsContextValue {
const ctx = useContext(SettingsContext);
if (!ctx) throw new Error('useSettings must be called within SettingsContext.Provider');
return ctx;
}
Router
Pages use useRouter(), not props
Pages access the router via context — no prop threading.
// Preferred — page gets router from context
export function TaskListPage() {
const { navigate } = useRouter();
// ...
}
// Avoid — threading navigate through route definitions
'/': { component: () => TaskListPage({ navigate: router.navigate }) }