Convert from Research Synthesiser-specific project to general-purpose ADS 3.0 design system intended to be forked for downstream applications. Add DESIGN.md following Google Labs spec as machine-readable reference for AI coding agents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
20 KiB
name, description, version, colors, typography, rounded, spacing, components
| name | description | version | colors | typography | rounded | spacing | components | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ADS 3.0 Design System | React component library implementing the NSW Department of Education's ADS 3.0 design language | alpha |
|
|
|
|
|
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 blue/grey palette with semantic colour 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 three layers:
- Palette — Raw values (
blue-01throughblue-05,grey-01throughgrey-04, etc.). Never use these directly in component code. - Semantic — Purpose-based aliases (
primary,error,success,warning,text,surface,bg,border). Use these for general UI. - Domain — Component-specific tokens (
button-default,control-border,badge-info,alert-error-bg,tag-navy, etc.). Use these within their respective components.
The primary brand colour is blue-01 (#002664, dark navy) for interactive elements like buttons and links. blue-02 (#146CFD) is the brighter accent used 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 600–700; 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 paddinggap-2(8px),gap-3(12px),gap-4(16px) for flex/grid gapsspace-y-4(16px) for stacked content
No custom spacing tokens are defined — the Tailwind defaults are sufficient. This may change in future.
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, tagsrounded-default(6px) — buttons, inputs, alertsrounded-lg(10px) — cards, dialogsrounded-xl(16px) — large containersrounded-full(9999px) — badges, circular buttons
Components
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)}
/>
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" />
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>
Chip
Selectable/dismissible filter chip.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
— | Required. Chip label |
selected |
boolean |
false |
Selected state |
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>
Tag
Coloured label for categorisation.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
'outline' | 'filled' | 'light' |
'outline' |
Visual style |
color |
'navy' | 'blue' | 'green' | 'red' | 'orange' | 'grey' |
'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>
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>
Card
Container with variant styles. Composed of Card, CardHeader, 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, CardContent, CardFooter } from '@/components/molecules/Card'
<Card variant="elevated">
<CardHeader action={<IconButton icon={<MoreHorizontal />} aria-label="Options" variant="tertiary" />}>
<h3>Card Title</h3>
</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. Composed of 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.
| 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 |
Composed with DialogHeader, DialogContent, DialogFooter.
import { Dialog, DialogHeader, DialogContent, DialogFooter } from '@/components/molecules/Dialog'
<Dialog open={isOpen} onClose={() => setIsOpen(false)} size="sm">
<DialogHeader onClose={() => setIsOpen(false)}>Confirm</DialogHeader>
<DialogContent>Are you sure you want to delete this?</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. Composed of Popover, PopoverTrigger, PopoverContent.
| 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 |
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>
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/utilsfor conditional classes - Use
forwardReffor components wrapping native elements - Use semantic HTML (
<button>,<input>,<dialog>) — not<div onClick> - Include
aria-labelon icon-only buttons - Use
lucide-reactfor icons (already a dev dependency)
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
labelprop on form controls — all inputs must have visible labels - Nest interactive elements (button inside button, link inside button)