300 HOURS
Responsive Web Design
Build five certification projects using HTML, CSS, flexbox, grid, and accessibility primitives.
freeCodeCamp presents
A dark-first, token-driven, framework-agnostic UI kit by the freeCodeCamp team. Built for learners-by learners. Open source and free to use.
design.freecodecamp.org/llms.txt
Components ship as source - copy them into your project (or point your coding agent at the link above) and make them yours. Full setup in the install guide.
00 · three surfaces, one system
01 · React
Copy-source React + TypeScript. Ark UI under the hood, React 18+ peer. You own the code.
design.freecodecamp.org/components/button.md
import { Button } from './ui/button/Button';02 · HTML
Drop-in vanilla CSS + 3 KB runtime. Download once, self-host.
<!-- download once from
design.freecodecamp.org/cdn/styles.min.css -->
<link rel="stylesheet" href="/styles.min.css">03 · Tailwind
Preset mirrors every token; palette variants fcc-dark / fcc-light.
// copy preset.ts + plugin.ts from
// design.freecodecamp.org/registry/tailwind.md
export default { presets: [fcc] };Foundations
The component layer is only honest if the token layer is. Six pillars define how surfaces read, resize, and respond.
Foundations
The whole kit reads from a small CSS custom-property layer. Swap a palette class on <html> and the entire surface changes - no component edits, no JavaScript.
Surface stack - primary through quaternary.
CTA + highlight pairs - gold-on-navy, click magnets.
Status colors - success / warning / danger / info.
02 · type & spacing
The body type ladder, monospaced display sizes, and the eight-step spacing grid every layout reaches for.
Type-heavy, shadow-free, accessible by default.
Eighteen-pixel minimum. Never compromise the body.
404-ROUTE - optimized for 120ch lines of code.
Group A
<Text />
Body-text primitive - semantic-tag agnostic, size + weight + tone variants.
Text
copy sourcesourceLarge body - section ledes and emphasis.
Default body - eighteen pixels minimum.
Caption - annotations, metadata, footnotes.
Bold - single-word emphasis only.
import { Text } from './ui/text/Text';
<Text size="lg">Large body - section ledes and emphasis.</Text>
<Text>Default body - eighteen pixels minimum.</Text>
<Text size="sm" tone="muted">Caption - annotations, metadata, footnotes.</Text><p class="text text--lg">Large body</p>
<p class="text">Default body</p>
<p class="text text--sm text--muted">Caption</p>--fs-body--fs-sm--fs-lg--foreground-secondary| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
as | enum | no | p | |
size | enum | no | base | |
weight | enum | no | regular | |
tone | enum | no | default |
<Heading />
Headline primitive - independent of HTML level. Five visual sizes; tag chosen for outline.
Heading
copy sourcesourceimport { Heading } from './ui/heading/Heading';
<Heading level={1} size="display">Command-line Chic.</Heading>
<Heading level={2} size="xl">Ship interfaces.</Heading>
<Heading level={3} size="lg">Composable primitives.</Heading>
<Heading level={4} size="md">Flat surfaces.</Heading>
<Heading level={5} size="sm">Square corners.</Heading><h1 class="heading heading--display">Command-line Chic.</h1>
<h2 class="heading heading--xl">Ship interfaces.</h2>
<h3 class="heading heading--lg">Composable primitives.</h3>--fs-display--fs-xl--fs-lg--fs-md| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
level | enum | no | 2 | |
size | enum | no | md |
<Badge />
Inline status chip - sparingly. Max 2 per row.
Badge
copy sourcesourceimport { Badge } from './ui/badge/Badge';
<Badge>Default</Badge>
<Badge variant="success">Passed</Badge>
<Badge variant="warning">In review</Badge>
<Badge variant="danger">Failed</Badge><span class="badge badge--success">Passed</span>
<span class="badge badge--warning">In review</span>
<span class="badge badge--danger">Failed</span>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | default |
<Avatar />
User mark - image, initials fallback, optional online/away status dot.
Avatar
copy sourcesourceimport { Avatar } from './ui/avatar/Avatar';
<Avatar size="sm" initials="RW" />
<Avatar size="md" initials="QC" status="online" />
<Avatar size="lg" src="/u/quincy.jpg" alt="Quincy Larson" status="away" /><span class="avatar avatar--md">
<span class="avatar__initials">QC</span>
<span class="avatar__status avatar__status--online"></span>
</span>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | yes | - | |
src | string | no | - | |
size | enum | no | md | |
status | enum | no | - |
<Divider />
Hairline rule. Solid or dashed; horizontal or vertical.
Divider
copy sourcesourceSolid
Dashed
import { Divider } from './ui/divider/Divider';
<Divider />
<Divider variant="dashed" />
<Divider orientation="vertical" /><hr class="divider" />
<hr class="divider divider--dashed" />
<span class="divider divider--vertical"></span>--border-strong| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
orientation | enum | no | horizontal | |
variant | enum | no | solid | |
decorative | boolean | no | false |
<Spacer />
Explicit whitespace on an 8-step scale.
Spacer
copy sourcesourceimport { Spacer } from './ui/spacer/Spacer';
<div style={{ display: 'flex' }}>
<span>start</span>
<Spacer size={6} />
<span>end</span>
</div><span class="spacer" style="width:48px"></span>--space-1--space-2--space-3--space-4--space-5| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
size | number | no | 4 | Step 0–8 maps onto the token scale (0, 4, 8, 12, 16, 24, 32, 48, 64 px). Any other number is treated as a raw pixel value. |
axis | enum | no | horizontal |
<Link />
Inline links carry the highlight accent and thicken on hover.
Link
copy sourcesourceExplore the token layer to recolor every component at once.
import { Link } from './ui/link/Link';
<p>Explore the <Link href="#tokens">token layer</Link> to recolor every component.</p><a class="fcc-link" href="#tokens">token layer</a>--highlight-color| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
block | boolean | no | false |
<Image />
Responsive image wrapper with built-in aspect caption and alt enforcement.
Image
copy sourcesourceimport { Image } from './ui/image/Image';
<Image src="/brand/fcc-secondary.svg" alt="freeCodeCamp mark" caption="freeCodeCamp mark" /><figure>
<img class="img--responsive" src="/brand/fcc-secondary.svg" alt="freeCodeCamp mark" />
<figcaption>freeCodeCamp mark</figcaption>
</figure>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
caption | ReactNode | no | - | |
figureClassName | string | no | |
Group B
<Button />
The workhorse. 3px square border, inverted fill on primary, no shadow.
Button
copy sourcesourceimport { Button } from './ui/button/Button';
export function Actions() {
return (
<div style={{ display: 'flex', gap: 12 }}>
<Button variant="cta">Start curriculum</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Dangerous</Button>
<Button variant="ghost">Ghost</Button>
</div>
);
}<button class="btn btn--cta">Start curriculum</button>
<button class="btn">Secondary</button>
<button class="btn btn--danger">Dangerous</button>
<button class="btn btn--ghost">Ghost</button>--cta-background--cta-foreground--foreground-primary| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | default | |
size | enum | no | md | |
block | boolean | no | false | |
isLoading | boolean | no | false |
<ToggleButton />
For stateful binary choices - hours pressed, unit selected, dark mode on.
Toggle button
copy sourcesourceimport { useState } from 'react';
import { ToggleButton } from './ui/toggle-button/ToggleButton';
export function Pressed() {
const [on, setOn] = useState(false);
return (
<ToggleButton pressed={on} onPressedChange={setOn}>
{on ? 'On' : 'Off'}
</ToggleButton>
);
}<button class="toggle-btn" aria-pressed="false">Off</button>
<button class="toggle-btn" aria-pressed="true">On</button>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
pressed | boolean | no | - | |
defaultPressed | boolean | no | - | |
onPressedChange | ((pressed: boolean) => void) | no | - | |
size | enum | no | md |
<CloseButton />
Inline dismiss affordance - modals, alerts, drawers.
Close button
copy sourcesourceimport { CloseButton } from './ui/close-button/CloseButton';
<CloseButton onClick={onDismiss} aria-label="Dismiss" /><button class="close-btn" aria-label="Dismiss">×</button>Group C
<Input />
Text input with a built-in label, helper text, and error slot.
Input
copy sourcesourceWe send one curriculum update per week.
import { FormGroup } from './ui/form-group/FormGroup';
import { Input } from './ui/input/Input';
import { HelpBlock } from './ui/help-block/HelpBlock';
<FormGroup>
<label htmlFor="email">Email address</label>
<Input id="email" type="email" placeholder="camper@example.com" />
<HelpBlock>We send one curriculum update per week.</HelpBlock>
</FormGroup><div class="form-group">
<label class="form-label" for="email">Email address</label>
<input class="input" id="email" type="email" placeholder="camper@example.com" />
<p class="form-help">We send one curriculum update per week.</p>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
invalid | boolean | no | - |
<Textarea />
Multi-line text control with mono variant for code snippets.
Textarea
copy sourcesourceMarkdown supported. 240 character cap.
import { Textarea } from './ui/textarea/Textarea';
<Textarea
id="bio"
rows={3}
placeholder="What are you learning right now?"
/><textarea class="input input--textarea" id="bio" rows="3"></textarea>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | default | |
invalid | boolean | no | - | |
autoResize | boolean | no | - |
<Select />
Native select wrapper - square chevron, matching height with Input.
Select
copy sourcesourceimport { Select } from './ui/select/Select';
<Select id="difficulty" defaultValue="intermediate">
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</Select><select class="select" id="difficulty">
<option>Beginner</option>
<option selected>Intermediate</option>
<option>Advanced</option>
</select>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
invalid | boolean | no | - |
<Checkbox />
Flat, square, accent-filled checkmark.
Checkbox
copy sourcesourceimport { Checkbox } from './ui/checkbox/Checkbox';
<Checkbox defaultChecked label="I accept the honor code" />
<Checkbox label="Email me certificate alerts" /><label class="check"><input type="checkbox" checked /> I accept the honor code</label>
<label class="check"><input type="checkbox" /> Email me certificate alerts</label>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | ReactNode | no | - | |
labelClassName | string | no | |
<Radio />
Mutually exclusive choice - group with `name` or `radio-group`.
Radio
copy sourcesourceimport { Radio, RadioGroup } from './ui/radio/Radio';
<RadioGroup name="theme" defaultValue="dark" label="Theme">
<Radio value="dark" label="Dark - default" />
<Radio value="light" label="Light" />
<Radio value="system" label="System" />
</RadioGroup><div class="radio-group" role="radiogroup">
<label class="radio"><input type="radio" name="theme" checked /> Dark</label>
<label class="radio"><input type="radio" name="theme" /> Light</label>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | ReactNode | no | - | |
labelClassName | string | no | |
<Switch />
For settings that take effect immediately - theme, audio, keybinds.
Switch
copy sourcesourceimport { Switch } from './ui/switch/Switch';
<Switch defaultChecked label="Keyboard shortcuts" />
<Switch label="Sound effects" /><label class="switch">
<input type="checkbox" checked />
<span class="switch__track"><span class="switch__thumb"></span></span>
<span class="switch__label">Keyboard shortcuts</span>
</label>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | ReactNode | no | - | |
labelClassName | string | no | |
<Fieldset />
Groups related controls under a legend - required for radio groups.
Fieldset
copy sourcesourceimport { Fieldset } from './ui/fieldset/Fieldset';
import { RadioGroup, Radio } from './ui/radio/Radio';
<Fieldset legend="Notification cadence">
<RadioGroup name="cadence" defaultValue="weekly">
<Radio value="weekly" label="Weekly digest" />
<Radio value="per-cert" label="Per-cert" />
<Radio value="never" label="Never" />
</RadioGroup>
</Fieldset><fieldset class="fieldset">
<legend class="fieldset__legend">Notification cadence</legend>
<label class="radio"><input type="radio" name="cadence" /> Weekly digest</label>
</fieldset>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
legend | ReactNode | no | - | |
tone | enum | no | default |
<FormControl />
Low-level text control - no label wrapper, for custom layouts.
Form control
copy sourcesourceimport { FormControl } from './ui/form-control/FormControl';
<FormControl placeholder="Search the curriculum" /><input class="input" placeholder="Search the curriculum" />| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
className | string | no | - | |
invalid | boolean | no | - | |
as | enum | no | - |
<FormGroup />
Groups a label, control, and help text with consistent spacing.
Form group
copy sourcesourceLetters, numbers, and dashes. Public.
import { FormGroup } from './ui/form-group/FormGroup';
import { Input } from './ui/input/Input';
import { HelpBlock } from './ui/help-block/HelpBlock';
<FormGroup>
<label htmlFor="username">Username</label>
<Input id="username" defaultValue="camper-42" />
<HelpBlock>Letters, numbers, and dashes. Public.</HelpBlock>
</FormGroup><div class="form-group">
<label class="form-label" for="username">Username</label>
<input class="input" id="username" value="camper-42" />
<p class="form-help">Letters, numbers, and dashes. Public.</p>
</div><HelpBlock />
Field-level guidance - neutral, success, or error tone.
Help block
copy sourcesourceWe send one curriculum update per week.
Username available.
Username already in use.
import { HelpBlock } from './ui/help-block/HelpBlock';
<HelpBlock>We send one curriculum update per week.</HelpBlock>
<HelpBlock variant="success">Username available.</HelpBlock>
<HelpBlock variant="error">Username already in use.</HelpBlock><p class="form-help">We send one curriculum update per week.</p>
<p class="form-help form-help--success">Username available.</p>
<p class="form-help form-help--error">Username already in use.</p>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | default |
<FormStepper />
Multi-step form progress - Ark UI machine, keyboard navigable.
Form stepper
copy sourcesourceimport { FormStepper } from './ui/form-stepper/FormStepper';
import { useState } from 'react';
const STEPS = [
{ id: 'account', label: 'Account', description: 'Email + handle' },
{ id: 'goals', label: 'Goals', description: 'What to learn first' },
{ id: 'confirm', label: 'Confirm', description: 'Review + start' }
];
const [current, setCurrent] = useState('account');
<FormStepper steps={STEPS} current={current} onStepChange={setCurrent} /><ol class="form-stepper">
<li class="form-stepper__step" aria-current="step">…</li>
</ol>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
steps | readonly FormStepperStep[] | yes | - | |
current | string | yes | - | |
onStepChange | (id: string) => void | yes | - | |
className | string | no | | |
isStepAccessible | ((step: FormStepperStep, state: StepState) => boolean) | no | - | Override step gating. Defaults to array-index comparison. |
ariaLabel | string | no | Progress |
Group D
<Sidebar />
Left rail - sections + items, optional collapsible groups.
Sidebar
copy sourcesourceimport { Sidebar } from './ui/sidebar/Sidebar';
<Sidebar
sections={[
{ id: 'primitives', label: 'Primitives', items: [
{ id: 'text', label: 'Text', href: '#text' },
{ id: 'button', label: 'Button', href: '#button', current: true }
]}
]}
/><aside class="sidebar">
<div class="sidebar__section">
<p class="sidebar__eyebrow">Primitives</p>
<a class="sidebar__item" href="#button" aria-current="true">Button</a>
</div>
</aside><Tabs />
Inverted-fill selected state, matches the platform editor panel.
Tabs
copy sourcesourceBuild a function that returns the sum of all odd Fibonacci numbers up to n.
// No output yet. Run tests to populate.
import { Tabs, Tab } from './ui/tabs/Tabs';
<Tabs defaultActiveKey="instructions">
<Tab eventKey="instructions" title="Instructions">...</Tab>
<Tab eventKey="tests" title="Tests">...</Tab>
<Tab eventKey="console" title="Console">...</Tab>
</Tabs><div class="tabs">
<div role="tablist" class="tabs__list">
<button class="tabs__tab" aria-selected="true">Instructions</button>
<button class="tabs__tab" aria-selected="false">Tests</button>
<button class="tabs__tab" aria-selected="false">Console</button>
</div>
<div class="tabs__panel">...</div>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
activeKey | string | no | - | |
defaultActiveKey | string | no | - | |
onSelect | ((key: string) => void) | no | - |
<Pagination />
Page selector - Ark UI pagination machine, keyboard navigable.
Pagination
copy sourcesourceimport { Pagination } from './ui/pagination/Pagination';
<Pagination
count={120}
pageSize={10}
page={2}
onPageChange={(page) => setPage(page)}
/><nav class="pagination" aria-label="Pagination">
<ul class="pagination__list">
<li><button class="pagination__btn" disabled>‹</button></li>
<li><button class="pagination__btn" aria-current="page">2</button></li>
<li><span class="pagination__ellipsis">…</span></li>
</ul>
</nav>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
count | number | yes | - | |
pageSize | number | yes | - | |
page | number | yes | - | |
siblingCount | number | no | 1 | |
onPageChange | ((page: number) => void) | no | - | |
prevLabel | ReactNode | no | Previous | |
nextLabel | ReactNode | no | Next |
<Listbox />
Persistent selection list - keyboard navigable, single or multi-select.
Listbox
copy sourcesourceimport { Listbox } from './ui/listbox/Listbox';
import { useState } from 'react';
const ITEMS = [
{ value: 'frontend', label: 'Frontend' },
{ value: 'backend', label: 'Backend' }
];
const [value, setValue] = useState<string | string[]>('frontend');
<Listbox items={ITEMS} value={value} onValueChange={setValue} /><ul class="listbox" role="listbox">
<li class="listbox__option" role="option" aria-selected="true">Responsive Web Design</li>
<li class="listbox__option" role="option">JavaScript Algorithms</li>
</ul>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
items | ListboxItem[] | yes | - | |
value | string | string[] | null | no | null | |
selectionMode | enum | no | single | |
onValueChange | ((value: string | string[]) => void) | no | - |
<Combobox />
Typeahead select - Ark UI combobox machine. Filters as you type.
Combobox
copy sourcesourceimport { Combobox, filterItemsByLabel } from './ui/combobox/Combobox';
import { useMemo, useState } from 'react';
const ALL = [
{ value: 'rwd', label: 'Responsive Web Design' },
{ value: 'js', label: 'JavaScript Algorithms' }
];
const [query, setQuery] = useState('');
const [value, setValue] = useState<string | null>(null);
const items = useMemo(() => filterItemsByLabel(ALL, query), [query]);
<Combobox
inputValue={query}
onInputValueChange={setQuery}
value={value}
onValueChange={setValue}
items={items}
placeholder='Pick a certification'
/><div class="combobox" role="combobox" aria-expanded="true">
<input class="combobox__input" type="text" />
<ul class="combobox__list" role="listbox">
<li class="combobox__item" role="option">Responsive Web Design</li>
</ul>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
items | ComboboxItem[] | yes | - | |
value | string | null | no | null | |
inputValue | string | no | - | |
placeholder | string | no | - | |
disabled | boolean | no | - | |
loading | boolean | no | - | When true, render a `data-part="loading"` row instead of empty/items. Useful during async fetches; pair with `useAsyncComboboxItems` for debounce + cancellation. |
error | ReactNode | no | - | Render a `data-part="error"` row with this message. Takes priority over the empty state so transient fetch errors surface clearly. |
emptyMessage | ReactNode | no | - | Message for the empty state. Rendered when `items.length === 0` and we're not loading. Defaults to "No results". |
loadingMessage | ReactNode | no | - | Message for the loading state. Defaults to "Loading…". |
onValueChange | ((value: string) => void) | no | - | |
onInputValueChange | ((inputValue: string) => void) | no | - | |
renderItem | ((item: ComboboxItem) => ReactNode) | no | - | |
aria-label | string | no | - | Defines a string value that labels the current element. |
aria-labelledby | string | no | - | Identifies the element (or elements) that labels the current element. |
<CommandPalette />
⌘K spotlight - global commands, grouped, keyboard-shortcut hinted.
Command palette
copy sourcesourceimport { CommandPalette } from './ui/command-palette/CommandPalette';
const GROUPS = [
{ label: 'Navigation', items: [
{ id: 'curriculum', label: 'Go to curriculum', shortcut: 'G C' }
]}
];
<CommandPalette
open={open}
onClose={() => setOpen(false)}
onSelect={id => navigate(id)}
groups={GROUPS}
placeholder='Type a command or search…'
/><div class="command-palette" role="dialog">
<input class="command-palette__search" type="text" />
<ul class="command-palette__list">…</ul>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
open | boolean | yes | - | |
onClose | () => void | yes | - | |
onSelect | (id: string) => void | yes | - | |
groups | readonly CommandPaletteGroup[] | yes | - | |
placeholder | string | no | Type a command… | |
emptyState | ReactNode | no | - | Slot rendered when `groups` is empty (after filtering). |
value | string | no | - | Controlled search value. Omit for uncontrolled. |
onValueChange | ((next: string) => void) | no | - | |
className | string | no | |
<Breadcrumb />
Hierarchical trail with the current page marked via aria-current.
Breadcrumb
copy sourcesourceimport { Breadcrumb } from './ui/breadcrumb/Breadcrumb';
<Breadcrumb>
<Breadcrumb.Item href="/">Docs</Breadcrumb.Item>
<Breadcrumb.Item href="/#navigation">Navigation</Breadcrumb.Item>
<Breadcrumb.Item active>Breadcrumb</Breadcrumb.Item>
</Breadcrumb><nav aria-label="Breadcrumb" class="breadcrumb">
<ol class="breadcrumb__list">
<li class="breadcrumb__item">
<a class="breadcrumb__link" href="/">Docs</a>
</li>
<li class="breadcrumb__item">
<a class="breadcrumb__link" href="/#navigation">Navigation</a>
</li>
<li class="breadcrumb__item">
<span class="breadcrumb__current" aria-current="page">Breadcrumb</span>
</li>
</ol>
</nav>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
aria-label | string | no | - | |
className | string | no | - |
Group E
<Modal />
Blocking dialog - confirmations and short-form destructive choices.
Modal
copy sourcesourceimport { Modal } from './ui/modal/Modal';
import { Button } from './ui/button/Button';
<Modal open={open} onClose={close} title="Reset progress?">
<Modal.Body>
You completed 17 of 32 steps in Responsive Web Design.
</Modal.Body>
<Modal.Footer>
<Button onClick={close}>Cancel</Button>
<Button variant="danger" onClick={reset}>Reset progress</Button>
</Modal.Footer>
</Modal><div class="modal" role="dialog" aria-labelledby="m-title">
<header class="modal__header">
<p class="modal__title" id="m-title">Reset progress?</p>
<button class="close-btn" aria-label="Close">×</button>
</header>
<div class="modal__body"><p>...</p></div>
<footer class="modal__footer">
<button class="btn">Cancel</button>
<button class="btn btn--danger">Reset progress</button>
</footer>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
open | boolean | yes | - | |
onClose | () => void | yes | - | |
title | ReactNode | no | - | |
closeOnBackdrop | boolean | no | true |
<Dropdown />
Menu anchored to a trigger - overflow actions, filters, sort.
Dropdown
copy sourcesourceimport { Dropdown } from './ui/dropdown/Dropdown';
<Dropdown>
<Dropdown.Toggle>Sort</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item>Most recent</Dropdown.Item>
<Dropdown.Item active>Alphabetical</Dropdown.Item>
<Dropdown.Item>Hardest first</Dropdown.Item>
</Dropdown.Menu>
</Dropdown><div class="dropdown">
<button class="btn" aria-expanded="true">Sort</button>
<div class="dropdown__menu">
<a class="dropdown__item" href="#">Most recent</a>
<a class="dropdown__item dropdown__item--active" href="#">Alphabetical</a>
</div>
</div><Tooltip />
Contextual hint on hover or focus - keep to 80 chars.
Tooltip
copy sourcesourceimport { Tooltip } from './ui/tooltip/Tooltip';
import { Button } from './ui/button/Button';
<Tooltip content="Runs the public test suite against your code.">
<Button>Run tests</Button>
</Tooltip><span class="tip" data-tip="Runs the public test suite against your code.">
<button class="btn">Run tests</button>
</span>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
content | ReactNode | yes | - | |
className | string | no | |
<Toast />
Transient status surface - Ark UI toaster machine, reduced-motion aware.
Toast
copy sourcesourceHeads up
Two failing tests since last run.
Build broken
Linter rejected your last commit.
New cert available
Backend Development is now public.
import { Toast, Toaster, createToaster } from './ui/toast/Toast';
// 1) Static - render Toast directly (the showcase variant).
<Toast variant='success' title='Saved' description='Synced.' />
// 2) Dynamic - drive a stack via createToaster + <Toaster>.
const toaster = createToaster({});
<Toaster toaster={toaster} />
toaster.create({ title: 'Saved', type: 'success' });<div class="toaster">
<div class="toast toast--success">
<div class="toast__title">Saved</div>
<div class="toast__description">Your progress is synced.</div>
<button class="toast__close">×</button>
</div>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | info | |
title | ReactNode | no | - | |
description | ReactNode | no | - | |
dismissible | boolean | no | true | Emit a close (`×`) button wired to `onDismiss`. Default: true. |
onDismiss | (() => void) | no | - |
Group F
<Alert />
Full-width status surface for page-level state - saved, failed, queued.
Alert
copy sourcesourceimport { Alert } from './ui/alert/Alert';
<Alert variant="success">All 28 tests pass. Next challenge unlocked.</Alert>
<Alert variant="warning">You have one unsaved edit.</Alert>
<Alert variant="danger">Sign-in failed - check your email address.</Alert><div class="alert alert--info">All 28 tests pass.</div>
<div class="alert alert--warning">You have one unsaved edit.</div>
<div class="alert alert--danger">Sign-in failed - check your email address.</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | info | |
title | ReactNode | no | - | |
icon | ReactNode | no | - |
<Callout />
Inline sidebar for curriculum notes, tips, and cautions.
Callout
copy sourcesourceimport { Callout } from './ui/callout/Callout';
<Callout variant="tip" label="Tip">
Open the editor fullscreen with <kbd>F11</kbd>.
</Callout><aside class="callout callout--tip">
<p class="callout__label">Tip</p>
<p class="callout__body">Open the editor fullscreen with <kbd>F11</kbd>.</p>
</aside>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | tip | |
label | ReactNode | no | - |
<Skeleton />
Placeholder bars during data fetch - reduced-motion aware shimmer.
Skeleton
copy sourcesourceimport { Skeleton } from './ui/skeleton/Skeleton';
<Skeleton variant="circle" size={48} />
<Skeleton variant="text" width="80%" />
<Skeleton variant="text" width="60%" /><span class="skeleton skeleton--circle"></span>
<span class="skeleton skeleton--text" style="width:80%"></span>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | rect | |
width | string | number | no | - | |
height | string | number | no | - | |
lines | number | no | - | For variant="text": render N stacked line bars. |
label | ReactNode | no | - | Screen-reader label announced via visually-hidden span. |
<EmptyState />
No-data placeholder with title, description, and a primary action.
Empty state
copy sourcesourcePick a track and complete five projects to claim your first cert.
import { EmptyState } from './ui/empty-state/EmptyState';
import { Button } from './ui/button/Button';
<EmptyState
title="No certifications yet"
description="Pick a track and complete five projects."
action={<Button variant="cta">Browse curriculum →</Button>}
/><div class="empty-state">
<p class="empty-state__title">No certifications yet</p>
<p class="empty-state__description">Pick a track…</p>
<div class="empty-state__action"><a class="btn btn--cta">Browse →</a></div>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
icon | ReactNode | no | - | |
title | ReactNode | no | - | |
description | ReactNode | no | - | |
action | ReactNode | no | - |
Group G
<Card />
Flat bordered container. Grid-friendly. No shadow, no hover lift.
Card
copy sourcesource300 HOURS
Build five certification projects using HTML, CSS, flexbox, grid, and accessibility primitives.
import { Card } from './ui/card/Card';
import { Link } from './ui/link/Link';
<Card>
<Card.Header>
<span className="card__dot card__dot--purple" aria-hidden="true" />
<p className="card__hours">300 HOURS</p>
</Card.Header>
<Card.Title>Responsive Web Design</Card.Title>
<Card.Body>Build five certification projects...</Card.Body>
<div className="progress">
<div className="progress__bar progress__bar--purple" style={{ width: '62%' }} />
</div>
<Card.Footer>
<span>62% complete</span>
<Link href="/learn/rwd">Resume →</Link>
</Card.Footer>
</Card><article class="card">
<header class="card__header">
<span class="card__dot card__dot--purple"></span>
<p class="card__hours">300 HOURS</p>
</header>
<h3 class="card__title">Responsive Web Design</h3>
<p class="card__body">Build five certification projects...</p>
<div class="progress"><div class="progress__bar progress__bar--purple" style="width:62%"></div></div>
</article>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
bordered | boolean | no | - |
<Panel />
Borderless surface-two container - sidebars, inspectors, nested regions.
Panel
copy sourcesourceEditor hints
import { Panel } from './ui/panel/Panel';
<Panel title="Editor hints">
Your code runs against the first test each time you save.
</Panel><div class="panel">
<p class="panel__heading">Editor hints</p>
<p class="panel__body">Your code runs against the first test each time you save.</p>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
variant | enum | no | default | |
title | ReactNode | no | - |
<Table />
Rule-heavy, square, mono-numeric - for progress and diagnostic data.
Table
copy sourcesource| Certification | Projects | Hours | Status |
|---|---|---|---|
| Responsive Web Design | 5 / 5 | 300 | Passed |
| JavaScript Algorithms | 4 / 5 | 300 | In review |
| Relational Databases | 0 / 5 | 300 | Not started |
import { Table } from './ui/table/Table';
import { Badge } from './ui/badge/Badge';
<Table>
<thead>
<tr><th>Certification</th><th>Projects</th><th>Status</th></tr>
</thead>
<tbody>
<tr><td>Responsive Web Design</td><td>5 / 5</td><td><Badge variant="success">Passed</Badge></td></tr>
</tbody>
</Table><table class="table">
<thead><tr><th>Certification</th><th>Projects</th><th>Status</th></tr></thead>
<tbody>
<tr><td>Responsive Web Design</td><td>5 / 5</td><td><span class="badge badge--success">Passed</span></td></tr>
</tbody>
</table>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
striped | boolean | no | - | |
condensed | boolean | no | - |
<DataTable />
Sortable, selectable, paginated table - header sort buttons, optional row select.
Data table
copy sourcesource| Cert | Hours | Status |
|---|---|---|
| Responsive Web Design | 300 | Passed |
| JavaScript Algorithms | 300 | In review |
import { DataTable } from './ui/data-table/DataTable';
import { useState } from 'react';
const [sortBy, setSortBy] = useState({ columnId: 'cert', direction: 'asc' });
<DataTable
columns={[
{ id: 'cert', header: 'Cert', sortable: true },
{ id: 'hours', header: 'Hours', align: 'right' },
{ id: 'status', header: 'Status', align: 'center' }
]}
rows={rows}
sortBy={sortBy}
onSortChange={setSortBy}
/><div class="data-table">
<table class="data-table__table">
<thead><tr><th class="data-table__header">Cert</th></tr></thead>
<tbody><tr><td class="data-table__cell">Responsive Web Design</td></tr></tbody>
</table>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
columns | readonly DataTableColumn<TRow>[] | yes | - | |
rows | readonly TRow[] | yes | - | |
rowId | ((row: TRow) => string) | no | <TRow,>(row: TRow): string => | Row id accessor. Defaults to `row.id`. |
sortBy | DataTableSort | null | no | - | |
onSortChange | ((next: DataTableSort | null) => void) | no | - | |
selection | ReadonlySet<string> | no | - | |
onSelectionChange | ((next: Set<string>) => void) | no | - | |
loading | boolean | no | false | |
emptyState | ReactNode | no | - | |
className | string | no | | |
caption | ReactNode | no | - | |
skeletonRows | number | no | 3 | Number of skeleton rows to emit while `loading`. |
<DescriptionList />
Term + detail metadata pair - stacked or inline layout.
Description list
copy sourcesourceimport { DescriptionList } from './ui/description-list/DescriptionList';
<DescriptionList
items={[
{ term: 'Username', detail: 'camper-42' },
{ term: 'Joined', detail: '2014-04-12' },
{ term: 'Certifications', detail: '3 of 14' }
]}
/><dl class="dl">
<dt class="dl__term">Username</dt>
<dd class="dl__detail">camper-42</dd>
</dl>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
items | DescriptionListItem[] | yes | - | |
layout | enum | no | vertical |
Group H
<SidebarLayout />
Header + left rail + main - the curriculum chrome shape.
Sidebar layout
copy sourcesourceimport { SidebarLayout } from './ui/sidebar-layout/SidebarLayout';
import { Sidebar } from './ui/sidebar/Sidebar';
import { Navbar } from './ui/navbar/Navbar';
<SidebarLayout
header={<Navbar … />}
aside={<Sidebar … />}
>
{children}
</SidebarLayout><div class="sidebar-layout">
<header class="sidebar-layout__header">…</header>
<div class="sidebar-layout__body">
<aside class="sidebar-layout__aside">…</aside>
<main class="sidebar-layout__main">…</main>
</div>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
header | ReactNode | no | - | |
sidebar | ReactNode | no | - |
<StackedLayout />
Header + main + footer - the marketing/news chrome shape.
Stacked layout
copy sourcesourceimport { StackedLayout } from './ui/stacked-layout/StackedLayout';
import { Navbar } from './ui/navbar/Navbar';
<StackedLayout
header={<Navbar … />}
footer={<footer>…</footer>}
>
{children}
</StackedLayout><div class="stacked-layout">
<header class="stacked-layout__header">…</header>
<main class="stacked-layout__main">…</main>
<footer class="stacked-layout__footer">…</footer>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
header | ReactNode | no | - | |
footer | ReactNode | no | - |
<AuthLayout />
Centered card on a tile-pattern backdrop - sign-in, sign-up, password reset.
Auth layout
copy sourcesourceimport { AuthLayout } from './ui/auth-layout/AuthLayout';
import { Input } from './ui/input/Input';
import { Button } from './ui/button/Button';
<AuthLayout brand="freeCodeCamp" footer={<Link href="/forgot">Forgot your password?</Link>}>
<Input label="Email" type="email" />
<Button variant="cta" block>Continue</Button>
</AuthLayout><div class="auth-layout auth-layout--pattern">
<div class="auth-layout__card">
<p class="auth-layout__brand">freeCodeCamp</p>
<button class="btn btn--cta btn--block">Continue</button>
<p class="auth-layout__footer"><a class="fcc-link">Forgot?</a></p>
</div>
</div>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
brand | ReactNode | no | - | |
footer | ReactNode | no | - | |
pattern | boolean | no | - |
Group I
<TileMatcher />
Data-driven memory / matching game for interactive curricula. Flip to reveal, match pairs, fire callbacks.
Tile matcher
copy sourcesourceimport { TileMatcher } from './ui/tile-matcher/TileMatcher';
const pairs = [
{ id: 'html', faces: ['HTML', 'Structure'] },
{ id: 'css', faces: ['CSS', 'Style'] },
{ id: 'js', faces: ['JS', 'Behavior'] }
];
export function Drill() {
return (
<TileMatcher
pairs={pairs}
columns={3}
onMatch={(id) => console.log('matched', id)}
onComplete={({ moves }) => console.log('done in', moves, 'moves')}
/>
);
}<!-- TileMatcher is a stateful React component.
Use the React package for curriculum embeds. -->
<div class="tile-matcher">
<div class="tile-matcher__grid">
<button class="tile-matcher__tile" data-state="down">…</button>
</div>
</div>--dur-base--ease-out--success-color--highlight-color| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
pairs | TileMatcherPair[] | yes | - | Deck definition. Tile count is `2 × pairs.length`. |
columns | number | no | - | Fixed column count. Omit for a responsive auto-fit grid. |
animateFlip | boolean | no | true | Flip animation on reveal. `false` swaps faces instantly. Default `true`. |
faceDown | boolean | no | true | Start tiles face-down (memory game). `false` shows every face. Default `true`. |
mismatchDelay | number | no | 900 | Delay before a mismatched pair flips back, in ms. Default `900`. |
disabled | boolean | no | false | Lock the whole board (no flips). |
shuffle | boolean | no | true | Shuffle the deck. Default `true`. |
seed | number | no | - | Seed for a deterministic shuffle (tests, visual snapshots). |
onMatch | ((pairId: string, tileIds: [string, string]) => void) | no | - | Fires when a pair is matched. |
onMismatch | ((tileIds: [string, string]) => void) | no | - | Fires when two flipped tiles do not match. |
onComplete | ((stats: { moves: number; matches: number; }) => void) | no | - | Fires once every pair is matched. |
<Hotspots />
Clickable regions overlaid on a background image or diagram. Quiz mode with target, feedback, and hints.
Hotspots
copy sourcesourceClick the fire
import { Hotspots } from './ui/hotspots/Hotspots';
import { RectHotspot, CircleHotspot, EllipseHotspot } from './ui/hotspot-shapes/HotspotShapes';
const Diagram = (): JSX.Element => (
<img src='/favicon.svg' alt='freecodecamp logo' />
);
const HOTSPOTS: HotspotItem[] = [
{
id: 'bracket-left',
label: 'Opening Paren',
shape: <RectHotspot x={33} y={25} width={29} height={92} />
},
{
id: 'fire',
label: 'Fire',
shape: <EllipseHotspot cx={100} cy={75} rx={30} ry={45} />
},
{
id: 'bracket-right',
label: 'Closing Paren',
shape: <RectHotspot x={138} y={25} width={29} height={92} />
}
];
export function HotspotsDemo(): JSX.Element {
return (
<div style={{ width: '100%', maxWidth: 360, margin: '0 auto' }}>
<Hotspots
background={<Diagram />}
width={200}
height={140}
hotspots={HOTSPOTS}
targetId='fire'
prompt='Click the fire'
onCorrect={id => console.log('correct', id)}
/>
</div>
);
}<!-- Hotspots is a stateful React component.
Use the React package for curriculum embeds. -->
<div class="hotspots">
<div class="hotspots__stage">
<div class="hotspots__background"><img src="/bird.png" alt="A songbird" /></div>
<svg class="hotspots__overlay" viewBox="0 0 200 140" preserveAspectRatio="none">
<g class="hotspots__hotspot" role="button" aria-label="Head">
<circle class="hotspots__shape" cx="55" cy="55" r="24" />
</g>
</svg>
</div>
</div>--highlight-color--success-color--danger-color--dur-fast| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
background | ReactNode | yes | - | Background layer: an image `src` string, or any node (component, `<svg>`). |
backgroundAlt | string | no | | Alt text used when `background` is an image `src` string. |
width | number | yes | - | Coordinate space width - hotspot geometry is expressed against this. |
height | number | yes | - | Coordinate space height. Also sets the container aspect ratio. |
hotspots | HotspotItem[] | yes | - | Clickable regions overlaid on the background. |
targetId | string | no | - | Quiz mode: the id of the correct hotspot. Omit for free selection. |
prompt | ReactNode | no | - | Instruction shown above the image (quiz mode). |
hintAfter | number | no | 3 | Reveal a hint naming the target after this many wrong attempts. Default `3`. |
selectedId | string | null | no | - | Controlled selection. Omit for uncontrolled. |
disabled | boolean | no | false | Lock the whole widget. |
onSelect | ((id: string) => void) | no | - | Fires on every pick with the chosen hotspot id. |
onCorrect | ((id: string) => void) | no | - | Quiz mode: fires when the target is picked. |
onIncorrect | ((id: string) => void) | no | - | Quiz mode: fires when a non-target is picked. |