Skip to main content

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 }) }