Skip to content

Documentation

Recipes

Copy-paste solutions for the patterns teams build most often with Cooud UI: validated forms, theme toggles, toasts, confirmations, and a command palette.

A validated form

Wire React Hook Form to a Zod schema with zodResolver. Each FormField binds a control to a field; FormMessage renders the validation error and FormLabel stays linked for accessibility. Reach for this whenever a form needs typed values and inline error feedback.

tsx
"use client";
import { Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from "@cooud-ui/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
const schema = z.object({
email: z.string().email("Enter a valid email address."),
password: z.string().min(8, "Use at least 8 characters."),
});
type Values = z.infer<typeof schema>;
export function SignInForm() {
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: { email: "", password: "" },
mode: "onChange",
});
const onSubmit = (values: Values) => {
console.log(values);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex max-w-md flex-col gap-5" noValidate>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@cooud.dev" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" autoComplete="current-password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Sign in</Button>
</form>
</Form>
);
}

Validate as you type

Set mode: "onChange" on useForm so errors surface while the visitor edits instead of only on submit. The resolver infers types from the schema, so z.infer<typeof schema> keeps your handler fully typed.

A dark-mode toggle

Flip between light and dark with the useTheme hook. It must be called inside a CooudUIProvider; toggleMode swaps the active mode and persists it through the provider's storageKey. Use this for the mode switch in a header or settings menu.

tsx
"use client";
import { Button } from "@cooud-ui/ui";
import { useTheme } from "@cooud-ui/theme";
import { Moon, Sun } from "lucide-react";
export function ModeToggle() {
const { mode, toggleMode } = useTheme();
const isDark = mode === "dark";
return (
<Button
variant="outline"
size="icon"
onClick={toggleMode}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
>
{isDark ? <Sun aria-hidden="true" /> : <Moon aria-hidden="true" />}
</Button>
);
}

To set a specific mode rather than toggle — for example a three-way light / dark / system control — call setMode directly.

tsx
import { useTheme } from "@cooud-ui/theme";
function ModePicker() {
const { mode, setMode } = useTheme();
return (
<div className="flex gap-2">
{(["light", "dark"] as const).map((value) => (
<button
key={value}
type="button"
onClick={() => setMode(value)}
aria-pressed={mode === value}
className="rounded-md border border-border px-3 py-1.5 text-sm text-fg-secondary aria-pressed:bg-surface-overlay aria-pressed:text-fg"
>
{value}
</button>
))}
</div>
);
}

Avoid a flash of the wrong mode

Render CooudThemeScript in the document head so the saved mode applies before paint. See Theming for the full setup.

Toast notifications

Mount a single <Toaster /> near the app root, then fire transient messages imperatively with toast.success(), toast.error(), or toast() from anywhere. Use toasts for non-blocking feedback after an action completes.

tsx
"use client";
import { Button, Toaster, toast } from "@cooud-ui/ui";
export function SaveBar() {
return (
<>
{/* Mount the Toaster once, near the app root. */}
<Toaster />
<div className="flex gap-2">
<Button onClick={() => toast.success("Changes saved successfully.")}>
Save
</Button>
<Button
variant="outline"
onClick={() => toast.error("Something went wrong. Please try again.")}
>
Trigger error
</Button>
</div>
</>
);
}

Mount the Toaster once

Render <Toaster /> a single time in a shared layout. The toast function then works from any component — including event handlers and async callbacks — without prop drilling.

Confirm a destructive action

Wrap a destructive trigger in an AlertDialog so the visitor must confirm before anything irreversible happens. Unlike a Dialog, an AlertDialog traps focus and expects an explicit confirm or cancel choice. Use it for delete, reset, or revoke flows.

tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
} from "@cooud-ui/ui";
import { Trash2 } from "lucide-react";
export function DeleteProjectButton({ onConfirm }: { onConfirm: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">
<Trash2 aria-hidden="true" />
Delete project
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
This permanently removes the project and all of its data. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Yes, delete it</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

Run the action on confirm

Attach your handler to AlertDialogAction's onClick. AlertDialogCancel dismisses without side effects, and both buttons close the dialog automatically.

A command palette

A CommandDialog opened by a button or the ⌘K / Ctrl+K shortcut, with grouped, searchable items. Use it as the keyboard-first way to jump between views and run actions across an app.

tsx
"use client";
import {
Button,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
Kbd,
} from "@cooud-ui/ui";
import { CalendarDays, Settings, User } from "lucide-react";
import { useEffect, useState } from "react";
export function CommandPalette() {
const [open, setOpen] = useState(false);
// Open with ⌘K / Ctrl+K.
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setOpen((value) => !value);
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
return (
<>
<Button variant="outline" onClick={() => setOpen(true)}>
Search
<Kbd className="ml-1">⌘K</Kbd>
</Button>
<CommandDialog
open={open}
onOpenChange={setOpen}
title="Command palette"
description="Search for a command to run."
>
<CommandInput placeholder="Type a command or search…" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem onSelect={() => setOpen(false)}>
<CalendarDays aria-hidden="true" />
Calendar
</CommandItem>
<CommandItem onSelect={() => setOpen(false)}>
<User aria-hidden="true" />
Search profile
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem onSelect={() => setOpen(false)}>
<Settings aria-hidden="true" />
Settings
<CommandShortcut>⌘S</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
}

Hint the shortcut

Show the trigger key with a Kbd badge, and add a per-item CommandShortcut so frequent actions are discoverable. Each CommandItem takes an onSelect handler that fires on click or Enter.