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.
"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><FormFieldcontrol={form.control}name="email"render={({ field }) => (<FormItem><FormLabel>Email</FormLabel><FormControl><Input type="email" placeholder="you@cooud.dev" {...field} /></FormControl><FormMessage /></FormItem>)}/><FormFieldcontrol={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
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.
"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 (<Buttonvariant="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.
import { useTheme } from "@cooud-ui/theme";function ModePicker() {const { mode, setMode } = useTheme();return (<div className="flex gap-2">{(["light", "dark"] as const).map((value) => (<buttonkey={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
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.
"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><Buttonvariant="outline"onClick={() => toast.error("Something went wrong. Please try again.")}>Trigger error</Button></div></>);}
Mount the Toaster once
<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.
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
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.
"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><CommandDialogopen={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
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.