Brainbox uses React 19, TailwindCSS v4, and Radix UI for building accessible, performant user interfaces. This guide covers component patterns and best practices.
Overview
UI components are organized in packages/ui/src/components/:
ui/ - Base components (Button, Dialog, Input, etc.)
Feature folders - Domain components (accounts/, databases/, pages/, etc.)
Components use TanStack Query for data fetching and mutations.
Architecture
Component → useMutation/useQuery → Client handlers → WebSocket/HTTP → Server
↓
Radix UI + TailwindCSS v4
Step-by-step guide
Create the component file
Place feature components in their domain folder: packages/ui/src/components/tasks/task-create.tsx
import { zodResolver } from '@hookform/resolvers/zod' ;
import { Plus } from 'lucide-react' ;
import { useForm } from 'react-hook-form' ;
import { toast } from 'sonner' ;
import { z } from 'zod/v4' ;
import { Button } from '@brainbox/ui/components/ui/button' ;
import {
Dialog ,
DialogContent ,
DialogDescription ,
DialogFooter ,
DialogHeader ,
DialogTitle ,
DialogTrigger ,
} from '@brainbox/ui/components/ui/dialog' ;
import {
Form ,
FormControl ,
FormField ,
FormItem ,
FormLabel ,
FormMessage ,
} from '@brainbox/ui/components/ui/form' ;
import { Input } from '@brainbox/ui/components/ui/input' ;
import {
Select ,
SelectContent ,
SelectItem ,
SelectTrigger ,
SelectValue ,
} from '@brainbox/ui/components/ui/select' ;
import { Spinner } from '@brainbox/ui/components/ui/spinner' ;
import { useMutation } from '@brainbox/ui/hooks/use-mutation' ;
const formSchema = z . object ({
name: z . string (). min ( 1 , 'Task name is required' ). max ( 200 ),
status: z . enum ([ 'todo' , 'in_progress' , 'done' ]). default ( 'todo' ),
assigneeId: z . string (). optional (),
});
interface TaskCreateProps {
parentId : string ;
workspaceId : string ;
accountId : string ;
onSuccess ?: ( taskId : string ) => void ;
}
export const TaskCreate = ({
parentId ,
workspaceId ,
accountId ,
onSuccess ,
} : TaskCreateProps ) => {
const [ open , setOpen ] = useState ( false );
const { mutate , isPending } = useMutation ();
const form = useForm < z . infer < typeof formSchema >>({
resolver: zodResolver ( formSchema ),
defaultValues: {
name: '' ,
status: 'todo' ,
},
});
const handleSubmit = async ( values : z . infer < typeof formSchema >) => {
mutate ({
input: {
type: 'task.create' ,
accountId ,
workspaceId ,
name: values . name ,
parentId ,
status: values . status ,
assigneeId: values . assigneeId ,
},
onSuccess ( output ) {
toast . success ( 'Task created' );
setOpen ( false );
form . reset ();
onSuccess ?.( output . id );
},
onError ( error ) {
toast . error ( error . message );
},
});
};
return (
< Dialog open = { open } onOpenChange = { setOpen } >
< DialogTrigger asChild >
< Button size = "sm" >
< Plus className = "size-4" />
New task
</ Button >
</ DialogTrigger >
< DialogContent >
< DialogHeader >
< DialogTitle > Create task </ DialogTitle >
< DialogDescription >
Add a new task to track work.
</ DialogDescription >
</ DialogHeader >
< Form { ... form } >
< form onSubmit = { form . handleSubmit ( handleSubmit ) } className = "space-y-4" >
< FormField
control = { form . control }
name = "name"
render = { ({ field }) => (
< FormItem >
< FormLabel > Task name </ FormLabel >
< FormControl >
< Input
placeholder = "Enter task name"
autoComplete = "off"
{ ... field }
/>
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< FormField
control = { form . control }
name = "status"
render = { ({ field }) => (
< FormItem >
< FormLabel > Status </ FormLabel >
< Select
onValueChange = { field . onChange }
defaultValue = { field . value }
>
< FormControl >
< SelectTrigger >
< SelectValue placeholder = "Select status" />
</ SelectTrigger >
</ FormControl >
< SelectContent >
< SelectItem value = "todo" > To do </ SelectItem >
< SelectItem value = "in_progress" > In progress </ SelectItem >
< SelectItem value = "done" > Done </ SelectItem >
</ SelectContent >
</ Select >
< FormMessage />
</ FormItem >
) }
/>
< DialogFooter >
< Button
type = "button"
variant = "outline"
onClick = { () => setOpen ( false ) }
>
Cancel
</ Button >
< Button type = "submit" disabled = { isPending } >
{ isPending && < Spinner className = "size-4" /> }
Create task
</ Button >
</ DialogFooter >
</ form >
</ Form >
</ DialogContent >
</ Dialog >
);
};
Use mutations for data changes
The useMutation hook handles client-side mutations: import { useMutation } from '@brainbox/ui/hooks/use-mutation' ;
const { mutate , isPending } = useMutation ();
mutate ({
input: {
type: 'task.create' ,
accountId ,
workspaceId ,
name: 'New task' ,
},
onSuccess ( output ) {
toast . success ( 'Task created' );
},
onError ( error ) {
toast . error ( error . message );
},
});
Use queries for data fetching
The useQuery hook fetches and subscribes to data: import { useQuery } from '@brainbox/ui/hooks/use-query' ;
const { data , isLoading , error } = useQuery ({
input: {
type: 'tasks.list' ,
accountId ,
workspaceId ,
parentId ,
},
});
if ( isLoading ) {
return < Spinner /> ;
}
if ( error ) {
return < div > Error: { error . message } </ div > ;
}
return (
< div >
{ data . tasks . map (( task ) => (
< TaskItem key = { task . id } task = { task } />
)) }
</ div >
);
Export and use the component
Export from the feature folder index: packages/ui/src/components/tasks/index.ts
export { TaskCreate } from './task-create' ;
export { TaskList } from './task-list' ;
export { TaskItem } from './task-item' ;
Use in your app: import { TaskCreate } from '@brainbox/ui/components/tasks' ;
< TaskCreate
parentId = { folderId }
workspaceId = { workspace . id }
accountId = { account . id }
onSuccess = { ( taskId ) => console . log ( 'Created:' , taskId ) }
/>
Component patterns
Use React Hook Form with Zod validation:
import { zodResolver } from '@hookform/resolvers/zod' ;
import { useForm } from 'react-hook-form' ;
import { z } from 'zod/v4' ;
const formSchema = z . object ({
name: z . string (). min ( 1 ). max ( 200 ),
email: z . string (). email (),
});
const form = useForm < z . infer < typeof formSchema >>({
resolver: zodResolver ( formSchema ),
defaultValues: {
name: '' ,
email: '' ,
},
});
const handleSubmit = ( values : z . infer < typeof formSchema >) => {
mutate ({
input: { type: 'user.update' , ... values },
onSuccess : () => toast . success ( 'Saved' ),
});
};
return (
< Form { ... form } >
< form onSubmit = { form . handleSubmit ( handleSubmit ) } >
{ /* form fields */ }
</ form >
</ Form >
);
Base UI components
Brainbox provides reusable base components built on Radix UI:
import { Button } from '@brainbox/ui/components/ui/button' ;
< Button variant = "default" size = "default" >
Click me
</ Button >
< Button variant = "destructive" size = "sm" >
Delete
</ Button >
< Button variant = "outline" size = "icon" >
< Plus className = "size-4" />
</ Button >
Loading states
Use the Spinner component for loading states:
import { Spinner } from '@brainbox/ui/components/ui/spinner' ;
if ( isLoading ) {
return (
< div className = "flex items-center justify-center p-8" >
< Spinner className = "size-6" />
</ div >
);
}
// In buttons
< Button disabled = { isPending } >
{ isPending && < Spinner className = "size-4" /> }
Save
</ Button >
Error handling
Display errors with toast notifications:
import { toast } from 'sonner' ;
mutate ({
input: { type: 'task.create' , ... },
onSuccess : () => {
toast . success ( 'Task created successfully' );
},
onError : ( error ) => {
toast . error ( error . message );
},
});
// Custom error messages
if ( error . code === 'FORBIDDEN' ) {
toast . error ( 'You do not have permission to perform this action.' );
} else {
toast . error ( 'Something went wrong. Please try again.' );
}
Optimistic updates
Update UI immediately before server confirmation:
const handleToggle = () => {
// Update local state immediately
setCompleted ( ! completed );
mutate ({
input: {
type: 'task.update' ,
id: task . id ,
completed: ! completed ,
},
onError : ( error ) => {
// Revert on error
setCompleted ( completed );
toast . error ( error . message );
},
});
};
Styling with TailwindCSS v4
Brainbox uses semantic color tokens:
< div className = "bg-bg-base text-fg-default" >
< h1 className = "text-fg-default font-semibold" > Title </ h1 >
< p className = "text-fg-muted text-sm" > Description </ p >
< Button className = "bg-accent-default hover:bg-accent-hover" >
Action
</ Button >
</ div >
Color tokens
Backgrounds : bg-base, bg-elevated, bg-subtle, bg-muted
Foregrounds : fg-default, fg-muted, fg-subtle
Borders : border-default, border-focus
Accent : accent-default, accent-hover, accent-active
Status : error, success, warning
Spacing and sizing
< div className = "space-y-4" >
< div className = "p-6 rounded-lg border border-border-default" >
< h2 className = "text-lg font-semibold mb-2" > Section </ h2 >
< p className = "text-sm text-fg-muted" > Content </ p >
</ div >
</ div >
Accessibility
Follow WAI-ARIA patterns and keyboard navigation:
// Proper focus management
< Dialog >
< DialogContent >
< Input autoFocus /> { /* Focus first input */ }
</ DialogContent >
</ Dialog >
// ARIA labels for icons
< Button variant = "ghost" size = "icon" aria-label = "Delete task" >
< Trash className = "size-4" />
</ Button >
// Semantic HTML
< nav >
< ul >
< li >< a href = "/tasks" > Tasks </ a ></ li >
</ ul >
</ nav >
Never disable zoom. Use font-size >= 16px for mobile inputs to prevent auto-zoom.
Avoid unnecessary re-renders
// Use React.memo for expensive components
const TaskItem = React . memo (({ task } : { task : Task }) => {
return < div > { task . name } </ div > ;
});
// Use useMemo for expensive calculations
const sortedTasks = useMemo (
() => tasks . sort (( a , b ) => a . order - b . order ),
[ tasks ]
);
// Use useCallback for event handlers passed to children
const handleClick = useCallback (
( id : string ) => {
navigate ( `/tasks/ ${ id } ` );
},
[ navigate ]
);
Virtualize long lists
Use virtua for rendering large lists:
import { VList } from 'virtua' ;
< VList style = { { height: '600px' } } >
{ tasks . map (( task ) => (
< TaskItem key = { task . id } task = { task } />
)) }
</ VList >
Testing components
Unit test
Integration test
import { render , screen , fireEvent } from '@testing-library/react' ;
import { TaskCreate } from './task-create' ;
test ( 'creates task' , async () => {
const onSuccess = vi . fn ();
render (
< TaskCreate
parentId = "parent-1"
workspaceId = "ws-1"
accountId = "acc-1"
onSuccess = { onSuccess }
/>
);
fireEvent . click ( screen . getByText ( 'New task' ));
fireEvent . change ( screen . getByLabelText ( 'Task name' ), {
target: { value: 'Test task' },
});
fireEvent . click ( screen . getByText ( 'Create task' ));
await waitFor (() => {
expect ( onSuccess ). toHaveBeenCalled ();
});
});
File locations
Base components: packages/ui/src/components/ui/[component].tsx
Feature components: packages/ui/src/components/[feature]/[component].tsx
Hooks: packages/ui/src/hooks/
Utilities: packages/ui/src/lib/