Files
ADS3-Design-System/DESIGN.md
Richie d36330084a Add CenteredPage template and fix SideNav collapse animation
Add CenteredPage template for no-sidebar, centered-content layouts
(login, error, onboarding). Fix SideNav collapse animation where items
slid right — removed nav-level items-center (each item already handles
its own centering) and added overflow-hidden to clip text during the
width transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-03 15:38:03 +10:00

42 KiB
Raw Permalink Blame History

name, description, version, colors, typography, rounded, spacing
name description version colors typography rounded spacing
ADS 3.0 Design System React component library implementing the NSW Department of Education's ADS 3.0 design language alpha
blue-01 blue-02 blue-03 blue-04 red-01 red-02 red-03 red-04 orange-01 orange-02 orange-03 orange-04 green-01 green-02 green-03 green-04 teal-01 teal-02 teal-03 teal-04 brown-01 brown-02 brown-03 brown-04 purple-01 purple-02 purple-03 purple-04 fuchsia-01 fuchsia-02 fuchsia-03 fuchsia-04 yellow-01 yellow-02 yellow-03 yellow-04 grey-01 grey-02 grey-03 grey-04 grey-05 white primary info secondary error success warning text text-secondary border bg surface control-border control-border-hover control-checked control-checked-hover control-focus-ring control-label control-description control-error control-bg control-bg-readonly button-default button-danger button-neutral button-subtle-bg button-subtle-text switch-on switch-on-hover
#002664 #146CFD #8CE0FF #CBEDFD #630019 #D7153A #FFB8C1 #FFE6EA #941B00 #F3631B #FFCE99 #FDEDDF #004000 #00AA45 #A8EDB3 #DBFADF #0B3F47 #2E808E #8CDBE5 #D1EEEA #523719 #B68D5D #E8D0B5 #EDE3D7 #441170 #8055F1 #CEBFFF #E6E1FD #65004D #D912AE #F4B5E6 #FDDEF2 #694800 #FAAF05 #FDE79A #FFF4CF #22272B #495054 #CDD3D6 #EBEBEB #F2F2F2 #FFFFFF {colors.blue-01} {colors.blue-02} {colors.blue-04} {colors.red-02} {colors.green-02} {colors.orange-02} {colors.grey-01} {colors.grey-02} {colors.grey-04} {colors.grey-05} {colors.white} {colors.grey-03} {colors.grey-01} {colors.blue-01} {colors.blue-02} {colors.blue-04} {colors.blue-01} {colors.grey-02} {colors.red-02} {colors.white} {colors.grey-05} {colors.blue-01} {colors.red-02} {colors.grey-01} {colors.blue-04} {colors.blue-01} {colors.green-02} {colors.green-01}
h1 h2 h3 h4 h5 h6 intro body small caption
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 48px 700 1.25
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 32px 700 1.25
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 24px 600 1.333
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 20px 600 1.4
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 16px 600 1.5
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 14px 600 1.43
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 20px 400 1.4
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 16px 400 1.5
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 14px 400 1.357
fontFamily fontSize fontWeight lineHeight
Public Sans Variable 12px 400 1.5
sm DEFAULT lg xl full
4px 8px 16px 24px 9999px
unit scale
4px Tailwind default (4px base: 1=4px, 2=8px, 3=12px, 4=16px, 6=24px, 8=32px)

Overview

ADS 3.0 (Adaptive Design System) is a React component library based on the NSW Department of Education's digital design language. It uses Public Sans as its typeface, a structured colour palette with semantic aliases, and consistent border radii and spacing. Components are built with React 19, TypeScript (strict), and Tailwind CSS v4.

All visual values come from design tokens defined in src/tokens/tokens.css. Components never use raw colour values — they reference semantic or domain-specific token classes via Tailwind utilities.

Colors

The palette is organised in layers, from raw to consumable:

  1. Palette — Raw values across 10 colour families (blue, red, orange, green, teal, brown, purple, fuchsia, yellow, grey) with 4 shades each (-01 darkest to -04 lightest), plus grey-05 and white. Never use these directly in component code.
  2. Semantic — Purpose-based aliases (primary, info, secondary, error, success, warning, text, text-secondary, surface, bg, border). Use these for general UI.
  3. Form control — Shared interactive-state tokens for all form components: control-border, control-border-hover, control-checked, control-checked-hover, control-focus-ring, control-label, control-description, control-error, control-bg, control-bg-readonly.
  4. Button — Intent tokens: button-default (navy), button-danger (red), button-neutral (dark grey), button-subtle-bg, button-subtle-text.
  5. Switch — On-state tokens: switch-on (success green), switch-on-hover.
  6. Badge — Status colour tokens: badge-navy, badge-info, badge-info-light, badge-success, badge-success-light, badge-error, badge-error-light, badge-warning, badge-warning-light, badge-neutral, plus contrast text tokens (badge-on-success-light, badge-on-error-light, badge-on-warning-light).
  7. Chip — Border/fill state tokens: chip-border, chip-text, chip-bg, chip-selected-bg, chip-selected-text.
  8. Tag — 11-colour system, each with a -light variant: navy, blue, green, red, orange, grey, teal, brown, purple, fuchsia, yellow.
  9. Alert — Background, border, and icon tokens for 5 variants: alert-{variant}-bg, alert-{variant}-border, alert-{variant}-icon (info, warning, error, success, neutral).
  10. Avataravatar (background), avatar-text.
  11. TopBartopbar (background, navy).
  12. SideNavnav-bg, nav-text, nav-icon, nav-active, nav-divider.

The primary brand colour is blue-01 (#002664, dark navy) used for buttons, links, and navigation. blue-02 (#146CFD) is the brighter accent for info states, focus rings, and highlights.

Typography

The system uses Public Sans Variable (loaded via @fontsource-variable/public-sans). The type scale runs from caption (12px) to h1 (48px). Headings use weight 600700; body text uses 400.

Use Tailwind's text-{scale} utilities: text-h1, text-h2, text-h3, text-h4, text-h5, text-h6, text-intro, text-body, text-small, text-caption.

Layout & Spacing

Spacing follows Tailwind's default 4px base scale. Common values:

  • p-2 (8px), p-3 (12px), p-4 (16px), p-6 (24px) for padding
  • gap-2 (8px), gap-3 (12px), gap-4 (16px) for flex/grid gaps
  • space-y-4 (16px) for stacked content

No custom spacing tokens are defined — the Tailwind defaults are sufficient.

Elevation & Depth

Two shadow levels:

  • shadow-default — subtle lift for cards and surfaces (0 1px 3px rgba(0,0,0,0.08))
  • shadow-md — elevated elements like dropdowns and dialogs (0 4px 6px -1px rgba(0,0,0,0.1))

Shapes

Border radii use the custom scale:

  • rounded-sm (4px) — small elements, tags
  • rounded-default (8px) — buttons, inputs, alerts, chips
  • rounded-lg (16px) — cards, dialogs
  • rounded-xl (24px) — large containers, page headers
  • rounded-full (9999px) — badges, avatars, circular buttons

Page Layout

Pages are composed using the AppShell template, which provides the fixed TopBar, collapsible SideNav, and scrollable content area. Content within the main area follows these conventions:

Shell structure

┌─────────────────────────────────────────┐
│ TopBar (h-16, fixed top, full width)    │
├────────┬────────────────────────────────┤
│SideNav │ Content area (scrollable)      │
│(w-[360]│                                │
│ or     │  PageHeader (optional)         │
│ w-20   │                                │
│collapsed│ Main content                  │
│)       │                                │
└────────┴────────────────────────────────┘

Content patterns

Dashboard — Use a responsive grid for content cards:

  • 2-column grid at lg breakpoint: grid grid-cols-1 lg:grid-cols-2 gap-6
  • Stat cards in a horizontal row: flex gap-4 or grid grid-cols-3 gap-4
  • Content padding: p-6 or p-8
  • Section spacing: space-y-6

List page — Stat summary row above a scrollable list:

  • Stat cards row: flex gap-4 with Card components
  • List section: Card wrapping a vertical list with divide-y divide-border
  • Each list item: px-6 py-4 with flex layout for metadata

Form page — Stepped form with vertical stepper:

  • Content max-width: max-w-3xl for readable line length
  • Form sections: space-y-6 between groups
  • Stepper: vertical list on left, form content fills remaining space
  • Submit actions: right-aligned with flex justify-end gap-3

Spacing conventions

  • Page content padding: p-6 (24px) minimum
  • Section gap: gap-6 (24px) between major sections
  • Card internal padding: p-5 or p-6
  • Form field spacing: space-y-4 (16px) between fields, space-y-6 (24px) between groups

Components

Atoms

Button

Interactive button with variant/intent/size matrix.

Prop Type Default Description
variant 'primary' | 'secondary' | 'tertiary' 'primary' Visual weight
intent 'default' | 'danger' | 'subtle' | 'neutral' 'default' Semantic purpose
size 'default' | 'comfortable' | 'compact' 'default' Height and padding
loading boolean false Shows spinner, disables interaction
leftIcon ReactNode Icon before label
rightIcon ReactNode Icon after label
import { Button } from '@/components/atoms/Button'

<Button variant="primary" intent="default">Save</Button>
<Button variant="secondary" intent="danger" leftIcon={<Trash2 />}>Delete</Button>
<Button variant="tertiary" size="compact">Cancel</Button>

IconButton

Icon-only button with required aria-label.

Prop Type Default Description
variant 'primary' | 'secondary' | 'tertiary' 'primary' Visual weight
intent 'default' | 'danger' | 'neutral' 'default' Semantic purpose
size 'default' | 'large' | 'compact' | 'small' | 'xsmall' 'default' Button dimensions
shape 'circle' | 'square' 'circle' Border radius
icon ReactNode Required. The icon element
aria-label string Required. Accessible label
import { IconButton } from '@/components/atoms/IconButton'
import { X, Settings } from 'lucide-react'

<IconButton icon={<X />} aria-label="Close" variant="tertiary" />
<IconButton icon={<Settings />} aria-label="Settings" shape="square" size="compact" />

Input

Text input with label, description, hint, error, and icon slots.

Prop Type Default Description
label string Required. Visible label
description string Help text below label
hint string Inline hint (right-aligned)
error string Error message (replaces description)
variant 'outlined' | 'stacked' 'outlined' Label position
size 'default' | 'compact' 'default' Input height
leftIcon ReactNode Icon inside input (left)
rightIcon ReactNode Icon inside input (right)
import { Input } from '@/components/atoms/Input'
import { Search, Mail } from 'lucide-react'

<Input label="Email" type="email" placeholder="you@example.com" leftIcon={<Mail />} />
<Input label="Search" variant="stacked" size="compact" leftIcon={<Search />} />
<Input label="Name" error="Name is required" />

Textarea

Multi-line text input with optional auto-resize.

Prop Type Default Description
label string Required. Visible label
description string Help text below label
hint string Inline hint
error string Error message
variant 'outlined' | 'stacked' 'outlined' Label position
resize 'vertical' | 'horizontal' | 'both' | 'none' 'vertical' Resize handle
autoResize boolean false Auto-grow with content
import { Textarea } from '@/components/atoms/Textarea'

<Textarea label="Notes" placeholder="Add your notes..." rows={4} />
<Textarea label="Description" autoResize error="Too short" />

Select

Custom dropdown select with keyboard navigation.

Prop Type Default Description
label string Required. Visible label
description string Help text
hint string Inline hint
error string Error message
variant 'outlined' | 'stacked' 'outlined' Label position
placeholder string Placeholder text
options SelectOption[] Required. { value, label, disabled? }
value string Controlled value
defaultValue string Uncontrolled default
onChange (value: string) => void Change handler
disabled boolean false Disables the select
import { Select } from '@/components/atoms/Select'

<Select
  label="Status"
  options={[
    { value: 'active', label: 'Active' },
    { value: 'archived', label: 'Archived' },
    { value: 'draft', label: 'Draft' },
  ]}
  onChange={(val) => console.log(val)}
/>

Autocomplete

Combobox input with filtering, keyboard navigation, and optional free-text entry.

Prop Type Default Description
label string Required. Visible label
description string Help text below label
hint string Inline hint
error string Error message
placeholder string Placeholder text
options AutocompleteOption[] Required. { value, label, disabled? }
value string Controlled value
onChange (value: string) => void Change handler
freeSolo boolean false Allow arbitrary text input
disabled boolean false Disables the input
loading boolean false Shows loading state
noResultsText string 'No results found' Empty state message
import { Autocomplete } from '@/components/atoms/Autocomplete'

<Autocomplete
  label="Country"
  options={[
    { value: 'au', label: 'Australia' },
    { value: 'nz', label: 'New Zealand' },
    { value: 'uk', label: 'United Kingdom' },
  ]}
  onChange={(val) => setCountry(val)}
/>
<Autocomplete label="Tags" freeSolo options={tagOptions} />

Checkbox

Checkbox with label, description, error, and indeterminate state.

Prop Type Default Description
label string Visible label
description string Help text below label
error string Error message
indeterminate boolean false Shows minus icon
import { Checkbox } from '@/components/atoms/Checkbox'

<Checkbox label="I agree to the terms" />
<Checkbox label="Select all" indeterminate description="Some items are selected" />
<Checkbox label="Required" error="You must agree" />

Radio / RadioGroup

Radio buttons grouped with shared state.

Prop (RadioGroup) Type Default Description
label string Group label
description string Group help text
error string Error message
value string Controlled value
defaultValue string Uncontrolled default
orientation 'vertical' | 'horizontal' 'vertical' Layout direction
onChange (value: string) => void Change handler
Prop (Radio) Type Default Description
label string Radio label
value string Required. Radio value
import { RadioGroup, Radio } from '@/components/atoms/Radio'

<RadioGroup label="Priority" orientation="horizontal" onChange={setPriority}>
  <Radio value="low" label="Low" />
  <Radio value="medium" label="Medium" />
  <Radio value="high" label="High" />
</RadioGroup>

Switch

Toggle switch with label and description.

Prop Type Default Description
label string Visible label
description string Help text
checked boolean false Controlled state
onChange (checked: boolean) => void Change handler
disabled boolean false Disables the switch
import { Switch } from '@/components/atoms/Switch'

<Switch label="Dark mode" checked={dark} onChange={setDark} />
<Switch label="Notifications" description="Receive email alerts" />

Slider

Single-value slider with step support and keyboard navigation.

Prop Type Default Description
label string Visible label
value number Required. Current value
onChange (value: number) => void Required. Change handler
min number 0 Minimum value
max number 100 Maximum value
step number 1 Step increment
disabled boolean false Disables the slider
import { Slider } from '@/components/atoms/Slider'

<Slider label="Volume" value={volume} onChange={setVolume} min={0} max={100} />
<Slider label="Opacity" value={opacity} onChange={setOpacity} min={0} max={1} step={0.1} />

RangeSlider

Dual-thumb range slider for selecting a value range.

Prop Type Default Description
label string Visible label
value [number, number] Required. Current range
onChange (value: [number, number]) => void Required. Change handler
min number 0 Minimum value
max number 100 Maximum value
step number 1 Step increment
disabled boolean false Disables the slider
import { RangeSlider } from '@/components/atoms/Slider'

<RangeSlider label="Price range" value={[20, 80]} onChange={setRange} min={0} max={200} />

FileInput

File picker with multi-file support and removable file tags.

Prop Type Default Description
label string Required. Visible label
description string Help text
error string Error message
accept string Accepted file types (e.g., .pdf,.docx)
multiple boolean false Allow multiple files
disabled boolean false Disables the input
onChange (files: File[]) => void Change handler
import { FileInput } from '@/components/atoms/FileInput'

<FileInput label="Upload documents" accept=".pdf,.docx" multiple onChange={setFiles} />
<FileInput label="Profile photo" accept="image/*" />

Badge

Small status indicator with colour variants.

Prop Type Default Description
variant 'navy' | 'info' | 'info-light' | 'success' | 'success-light' | 'error' | 'error-light' | 'warning' | 'warning-light' | 'neutral' | 'white' 'navy' Colour variant
leftIcon ReactNode Icon before text
rightIcon ReactNode Icon after text
import { Badge } from '@/components/atoms/Badge'

<Badge variant="success">Active</Badge>
<Badge variant="error-light">Overdue</Badge>
<Badge variant="info" leftIcon={<Clock />}>Pending</Badge>

Tag

Coloured label for categorisation.

Prop Type Default Description
variant 'outline' | 'filled' | 'light' 'outline' Visual style
color 'navy' | 'blue' | 'green' | 'red' | 'orange' | 'grey' | 'teal' | 'brown' | 'purple' | 'fuchsia' | 'yellow' 'navy' Colour
size 'default' | 'sm' 'default' Tag size
icon ReactNode Leading icon
onRemove () => void Shows remove button
import { Tag } from '@/components/atoms/Tag'

<Tag color="blue" variant="light">Research</Tag>
<Tag color="green" variant="filled" onRemove={handleRemove}>Complete</Tag>

Chip

Selectable/dismissible filter chip with optional colour variants.

Prop Type Default Description
children ReactNode Required. Chip label
selected boolean false Selected state
color 'default' | 'info' | 'error' | 'warning' | 'success' Colour variant (unselected only)
onDismiss () => void Shows dismiss icon, makes removable
rightIcon ReactNode Custom right icon
import { Chip } from '@/components/atoms/Chip'

<Chip selected={isActive} onClick={toggle}>Qualitative</Chip>
<Chip onDismiss={() => removeFilter('date')}>2024</Chip>
<Chip color="success">Approved</Chip>

Tabs

Tab navigation with controlled selection. Compound component: Tabs, TabList, Tab, TabPanel.

Prop (Tabs) Type Default Description
value string Required. Active tab value
onChange (value: string) => void Required. Tab change handler
Prop (Tab) Type Default Description
value string Required. Tab identifier
icon ReactNode Icon before label
disabled boolean false Disables the tab
Prop (TabPanel) Type Default Description
value string Required. Must match a Tab value
import { Tabs, TabList, Tab, TabPanel } from '@/components/atoms/Tabs'

<Tabs value={activeTab} onChange={setActiveTab}>
  <TabList>
    <Tab value="overview">Overview</Tab>
    <Tab value="details" icon={<Info />}>Details</Tab>
    <Tab value="settings">Settings</Tab>
  </TabList>
  <TabPanel value="overview">Overview content</TabPanel>
  <TabPanel value="details">Details content</TabPanel>
  <TabPanel value="settings">Settings content</TabPanel>
</Tabs>

List

Vertical list with icon support, active state, and optional links. Compound component: List, ListItem, ListSubheader, ListDivider.

Prop (ListItem) Type Default Description
icon ReactNode Leading icon
active boolean false Highlighted state
disabled boolean false Disables the item
href string Renders as <a> instead of <li>
import { List, ListItem, ListSubheader, ListDivider } from '@/components/atoms/List'
import { Home, Settings, Users } from 'lucide-react'

<List>
  <ListSubheader>Navigation</ListSubheader>
  <ListItem icon={<Home />} active>Dashboard</ListItem>
  <ListItem icon={<Users />}>Team</ListItem>
  <ListDivider />
  <ListItem icon={<Settings />}>Settings</ListItem>
</List>

Avatar

Circular avatar showing initials or an image.

Prop Type Default Description
initials string Required. Fallback text (first 2 chars, uppercased)
src string Image URL
alt string Image alt text
size 'sm' | 'default' | 'lg' 'default' Avatar dimensions (32/40/48px)
import { Avatar } from '@/components/atoms/Avatar'

<Avatar initials="JD" />
<Avatar initials="MM" src="/photos/mary.jpg" alt="Mary McKay" size="lg" />

Tooltip

Floating tooltip on hover/focus with arrow.

Prop Type Default Description
content ReactNode Required. Tooltip content
placement Floating UI Placement 'top' Position relative to trigger
delay number | { open?, close? } { open: 400, close: 0 } Show/hide delay (ms)
children ReactElement Required. Trigger element
import { Tooltip } from '@/components/atoms/Tooltip'

<Tooltip content="Save your changes">
  <Button>Save</Button>
</Tooltip>

Molecules

Card

Container with variant styles. Compound component: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter.

Prop (Card) Type Default Description
variant 'surface' | 'outlined' | 'elevated' | 'filled' 'surface' Visual style
Prop (CardHeader) Type Default Description
action ReactNode Action element (top-right)
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/molecules/Card'

<Card variant="elevated">
  <CardHeader action={<IconButton icon={<MoreHorizontal />} aria-label="Options" variant="tertiary" />}>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>Supporting description text</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Card body content goes here.</p>
  </CardContent>
  <CardFooter>
    <Button variant="tertiary" size="compact">View details</Button>
  </CardFooter>
</Card>

Accordion

Collapsible content sections. Compound component: Accordion, AccordionItem, AccordionTrigger, AccordionContent.

Prop (Accordion) Type Default Description
type 'single' | 'multiple' 'single' One or many open at once
collapsible boolean false Allow closing all (single mode)
defaultValue string | string[] Initially open item(s)
value string | string[] Controlled open state
onValueChange (value) => void Change handler
Prop (AccordionItem) Type Default Description
value string Required. Unique item identifier
disabled boolean false Prevents opening
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/molecules/Accordion'

<Accordion type="single" collapsible>
  <AccordionItem value="section-1">
    <AccordionTrigger>What is this?</AccordionTrigger>
    <AccordionContent>Explanation here.</AccordionContent>
  </AccordionItem>
  <AccordionItem value="section-2">
    <AccordionTrigger>How does it work?</AccordionTrigger>
    <AccordionContent>Details here.</AccordionContent>
  </AccordionItem>
</Accordion>

Alert

Contextual message with icon, title, close button, and action slot.

Prop Type Default Description
variant 'info' | 'warning' | 'error' | 'success' | 'neutral' 'info' Alert type and colour
title string Bold heading
onClose () => void Shows close button
action ReactNode Action element (e.g. link or button)
icon ReactNode Custom icon (default based on variant)
import { Alert } from '@/components/molecules/Alert'

<Alert variant="success" title="Saved" onClose={dismiss}>
  Your changes have been saved successfully.
</Alert>
<Alert variant="error" title="Error">Something went wrong.</Alert>

Dialog

Modal dialog with backdrop, size options, and composed sections. Uses the native <dialog> element. Compound component: Dialog, DialogHeader, DialogTitle, DialogDescription, DialogContent, DialogFooter, DialogClose.

Prop (Dialog) Type Default Description
open boolean Required. Controls visibility
onClose () => void Required. Close handler
size 'sm' | 'default' | 'lg' | 'full' 'default' Max width
closeOnBackdrop boolean true Close when clicking backdrop
Prop (DialogHeader) Type Default Description
onClose () => void Shows close button in header
Prop (DialogClose) Type Default Description
onClose () => void Required. Close handler
children ReactNode Required. Button content
import { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogContent, DialogFooter } from '@/components/molecules/Dialog'

<Dialog open={isOpen} onClose={() => setIsOpen(false)} size="sm">
  <DialogHeader onClose={() => setIsOpen(false)}>
    <DialogTitle>Confirm deletion</DialogTitle>
    <DialogDescription>This action cannot be undone.</DialogDescription>
  </DialogHeader>
  <DialogContent>Are you sure you want to delete this item?</DialogContent>
  <DialogFooter>
    <Button variant="tertiary" onClick={() => setIsOpen(false)}>Cancel</Button>
    <Button intent="danger">Delete</Button>
  </DialogFooter>
</Dialog>

Popover

Floating content panel triggered by a child element. Compound component: Popover, PopoverTrigger, PopoverContent, PopoverClose.

Prop (Popover) Type Default Description
placement Floating UI Placement 'bottom' Position relative to trigger
open boolean Controlled open state
onOpenChange (open: boolean) => void Open state handler
Prop (PopoverClose) Type Default Description
onClose () => void Required. Close handler
children ReactNode Required. Button content
import { Popover, PopoverTrigger, PopoverContent } from '@/components/molecules/Popover'

<Popover placement="bottom-start">
  <PopoverTrigger>
    <Button variant="secondary">Options</Button>
  </PopoverTrigger>
  <PopoverContent>
    <p>Popover content here.</p>
  </PopoverContent>
</Popover>

DataTable

Data table with sorting, row selection, and pagination. Generic component supporting typed row data.

Prop Type Default Description
columns DataTableColumn<T>[] Required. Column definitions
data T[] Required. Row data array
selectable boolean false Enable row selection with checkboxes
pagination boolean true Show pagination controls
pageSize number 5 Initial rows per page
pageSizeOptions number[] [5, 10, 25] Page size dropdown options
loading boolean false Shows loading state
emptyMessage string 'No data available' Empty state text
onSelectionChange (selected: T[]) => void Selection change handler

DataTableColumn:

Property Type Default Description
key string Required. Row data key
header string Required. Column header text
sortable boolean false Enable column sorting
align 'left' | 'center' | 'right' 'left' Text alignment
render (value, row, index) => ReactNode Custom cell renderer
import { DataTable } from '@/components/molecules/DataTable'

<DataTable
  columns={[
    { key: 'name', header: 'Name', sortable: true },
    { key: 'email', header: 'Email' },
    { key: 'role', header: 'Role', sortable: true },
    { key: 'status', header: 'Status', render: (val) => <Badge variant={val === 'active' ? 'success' : 'neutral'}>{val}</Badge> },
  ]}
  data={users}
  selectable
  pageSize={10}
  onSelectionChange={setSelected}
/>

Organisms

TopBar

Fixed application header with logo, title, and action slots.

Prop Type Default Description
title string Required. Application title
leading ReactNode Leading element (e.g., hamburger menu)
logo ReactNode Logo element next to title
children ReactNode Trailing actions (e.g., notifications, avatar)
import { TopBar } from '@/components/organisms/TopBar'

<TopBar
  title="Performance and development plan"
  leading={<IconButton icon={<Menu />} aria-label="Menu" variant="tertiary" />}
  logo={<img src="/nsw-logo.svg" alt="NSW" className="h-7" />}
>
  <IconButton icon={<Bell />} aria-label="Notifications" variant="tertiary" />
  <Avatar initials="MM" size="sm" />
</TopBar>

SideNav

Collapsible sidebar navigation with grouped items, badges, and alert indicators. Compound component: SideNav, SideNavItem, SideNavGroup, SideNavDivider.

Prop (SideNav) Type Default Description
collapsed boolean false Collapsed to icon-only mode (w-20)
Prop (SideNavItem) Type Default Description
icon ReactNode Leading icon
active boolean false Active/current state
badge number Count badge
alert boolean | 'info' | 'success' | 'warning' | 'error' Alert dot indicator
href string Renders as <a> instead of <button>
Prop (SideNavGroup) Type Default Description
icon ReactNode Required. Group icon
label string Required. Group label
defaultOpen boolean false Initially expanded
badge number Count badge
alert boolean | 'info' | 'success' | 'warning' | 'error' Alert dot indicator
active boolean false Highlight the group trigger
import { SideNav, SideNavItem, SideNavGroup, SideNavDivider } from '@/components/organisms/SideNav'
import { Home, FileText, Settings, Users } from 'lucide-react'

<SideNav collapsed={isCollapsed}>
  <SideNavItem icon={<Home />} active>My status</SideNavItem>
  <SideNavItem icon={<FileText />}>My details</SideNavItem>
  <SideNavDivider />
  <SideNavGroup icon={<Users />} label="PDP" defaultOpen>
    <SideNavItem active>My PDP</SideNavItem>
    <SideNavItem>PDP guide</SideNavItem>
    <SideNavItem>Management</SideNavItem>
  </SideNavGroup>
  <SideNavItem icon={<Settings />}>Settings</SideNavItem>
</SideNav>

PageHeader

Page title section with themed background, decorative arcs, and optional icon.

Prop Type Default Description
title string Required. Page heading
subtitle string Supporting text below title
icon ReactNode Leading icon or illustration
iconSize string '50px' Icon width/height
theme 'light' | 'dark' 'light' Background theme (light: blue-04 bg, dark: navy bg)
centered boolean false Center-aligned layout
noBackground boolean false No background or decorative arcs
children ReactNode Additional content below title
import { PageHeader } from '@/components/organisms/PageHeader'

<PageHeader title="My Workspace" subtitle="Accreditation Level: Maintaining Proficient Teacher">
  <div className="flex gap-4 mt-2">
    <Badge variant="info">Maintaining</Badge>
    <Badge variant="navy">5 Years</Badge>
  </div>
</PageHeader>
<PageHeader title="Welcome" theme="dark" centered />
<PageHeader title="Settings" noBackground />

Templates

Templates are page-level layout components that define where content goes via typed slot props. They do not own content. Use them inside AppShell for full page layouts, or standalone for the content area only.

AppShell

Application shell that composes TopBar + SideNav + scrollable content area. All pages render inside this.

Prop Type Default Description
topBar ReactNode Required. TopBar component
sideNav ReactNode Required. SideNav component
sideNavCollapsed boolean false Whether SideNav is in icon-only mode
children ReactNode Scrollable content area
import { AppShell } from '@/components/templates/AppShell'

<AppShell
  topBar={<TopBar title="My App" />}
  sideNav={<SideNav>{/* nav items */}</SideNav>}
>
  {/* Page content */}
</AppShell>

DashboardPage

Dashboard layout with stat summary row and responsive 2-column content grid.

Prop Type Default Description
header ReactNode PageHeader or custom header
stats ReactNode Stat cards row (flex wrapped)
children ReactNode Required. Content grid items (2-col at lg)
import { DashboardPage } from '@/components/templates/DashboardPage'

<DashboardPage
  header={<PageHeader title="Dashboard" />}
  stats={<><StatCard /><StatCard /><StatCard /></>}
>
  <Card>Left column</Card>
  <Card>Right column</Card>
</DashboardPage>

ListPage

List layout with stat summary, list header with actions, and scrollable item list.

Prop Type Default Description
header ReactNode PageHeader or custom header
stats ReactNode Stat cards row
listHeader ReactNode Title + action button above the list
children ReactNode Required. List items (rendered inside divide-y container)
import { ListPage } from '@/components/templates/ListPage'

<ListPage
  header={<PageHeader title="Activity Log" />}
  stats={<><StatCard /><StatCard /></>}
  listHeader={<div className="flex justify-between"><h2>Items</h2><Button>Add</Button></div>}
>
  <div className="px-6 py-4">Item 1</div>
  <div className="px-6 py-4">Item 2</div>
</ListPage>

FormPage

Form layout with optional vertical stepper and constrained-width content.

Prop Type Default Description
header ReactNode PageHeader or custom header
actions ReactNode Action bar above form (dropdowns, buttons)
steps FormPageStep[] Vertical stepper steps ({ label, status })
children ReactNode Required. Form content (max-w-3xl centered)

FormPageStep:

Property Type Description
label string Step label text
status 'complete' | 'current' | 'upcoming' Step state
import { FormPage } from '@/components/templates/FormPage'

<FormPage
  header={<PageHeader title="Setup" />}
  steps={[
    { label: 'Your details', status: 'current' },
    { label: 'Review', status: 'upcoming' },
  ]}
>
  <Card><CardContent><Input label="Name" /></CardContent></Card>
</FormPage>

DetailPage

Single-column detail view for records, profiles, or documents. Constrained width for readability.

Prop Type Default Description
header ReactNode PageHeader or custom header
actions ReactNode Action bar below header (e.g. tabs, buttons)
maxWidth 'md' | 'lg' | 'xl' | 'full' 'xl' Content area max width
children ReactNode Required. Page content
import { DetailPage } from '@/components/templates/DetailPage'

<DetailPage
  header={<PageHeader title="Alex Chen" subtitle="Senior Engineer" />}
  actions={<Tabs value={tab} onChange={setTab}><TabList><Tab value="overview">Overview</Tab></TabList></Tabs>}
  maxWidth="lg"
>
  <Card><CardContent>Detail content</CardContent></Card>
</DetailPage>

CenteredPage

Full-page layout with no sidebar and centered content. Use for login, sign-up, error pages, onboarding, or any focused single-task flow.

Prop Type Default Description
topBar ReactNode Optional TopBar
maxWidth 'sm' | 'md' | 'lg' | 'xl' 'md' Content max width
children ReactNode Required. Centered content
import { CenteredPage } from '@/components/templates/CenteredPage'

<CenteredPage topBar={<TopBar title="" logo={<NswLogo />} />} maxWidth="md">
  <Card variant="elevated">
    <CardContent>Login form here</CardContent>
  </Card>
</CenteredPage>

Do's and Don'ts

Do:

  • Use semantic token classes (bg-primary, text-error, border-control-border), never palette tokens (bg-blue-01)
  • Use the cn() utility from @/lib/utils for conditional classes
  • Use forwardRef for components wrapping native elements
  • Use semantic HTML (<button>, <input>, <dialog>) — not <div onClick>
  • Include aria-label on icon-only buttons
  • Use lucide-react for icons (already a dev dependency)
  • Compose pages using AppShell (TopBar + SideNav + content) as the outer layout
  • Use the spacing conventions (p-6 content padding, gap-6 between sections)

Don't:

  • Hardcode colour hex values in component code
  • Use inline styles (except for truly dynamic values like calculated positions)
  • Use CSS modules or styled-components
  • Skip the label prop on form controls — all inputs must have visible labels
  • Nest interactive elements (button inside button, link inside button)
  • Reference palette tokens (text-blue-01, border-grey-03) in component code — add a semantic token if one doesn't exist