Handbook

One page. Every rule.

Everything a builder needs to ship fluently with freeCodeCamp UIKit - tokens, type, motion, voice, brand, install, bundle, and how to contribute. Use the handbook navigation to jump between rules.

Foundations · 01

Colors

Navy-dark base, gold CTA, terminal semantics. The palette is driven by CSS custom properties so every surface re-themes together.

Palette roots

The dark palette is the source of truth. Switch the .light-palette class on <html> and every token remaps without re-rendering any component.

  • --gray-00

    #ffffff

    Foreground base - dark mode

  • --gray-05

    #f5f6f7

  • --gray-10

    #dfdfe2

  • --gray-45

    #858591

    Muted body copy

  • --gray-75

    #3b3b4f

  • --gray-85

    #1b1b32

    Secondary surfaces

  • --gray-90

    #0a0a23

    Primary surface - "dark night sky"

Semantic surfaces

Every component reaches for these five surface tokens instead of raw grays. Swap a surface once and the whole kit follows.

  • --background-primary

    var(--gray-90)

  • --background-secondary

    var(--gray-85)

  • --background-tertiary

    #33334f

  • --background-quaternary

    #4b4b66

  • --foreground-primary

    var(--gray-00)

  • --foreground-muted

    #b0b0bd

    Captions, metadata

Intent colors

Semantic pairs - a foreground for on-surface text and a background for solid fills - so success, danger, warning, highlight, purple, and love each carry their own readable on-dark / on-light variants.

  • --cta-background

    #ffbf00

    Primary CTA fill

  • --highlight-color

    #99c9ff

    Links, active nav

  • --success-color

    #acd157

  • --warning-color

    #f1be32

  • --danger-color

    #ffadad

  • --purple-color

    #dbb8ff

  • --love-color

    #f8577c

    Donate, heart affordances

Using colour in code

Reach for the semantic token, never a raw hex. If you need a new shade, add it to packages/uikit-css/src/tokens.css first so every surface picks it up.

.my-card {
  background: var(--background-secondary);
  color: var(--foreground-primary);
  border: 1px solid var(--border-strong);
}

A scoped preview of both palette roots - flipping .light-palette on <html> toggles every surface together, but you can also apply the class to any subtree for per-region theming. Each tile below is its own palette root.

Foundations · 02

Typography

Lato carries body and UI copy. Hack-ZeroSlash carries code, metadata, and anything mono-spaced. Both ship with the package; no external font CDN.

Scale

Six steps - small enough to keep the system honest, wide enough to express hierarchy without ornamental sizes. Every scale step is a token so redefining the scale per palette is a one-line change.

  • Display

    --fs-3xl

    56px · --lh-tight

    Build with UIKit.

  • Heading 1

    --fs-2xl

    42px · --lh-snug

    Ship with UIKit.

  • Heading 2

    --fs-xl

    32px · --lh-snug

    Tokens are API.

  • Heading 3

    --fs-lg

    24px · --lh-snug

    Motion is honest.

  • Body

    --fs-md

    18px · --lh-base

    Readable prose. Mono metadata.

  • Small · Mono

    --fs-sm

    16px · eyebrows, kbd, meta

    curl design.freecodecamp.org/llms.txt

Stacks

--font-sans: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Hack-ZeroSlash', 'Fira Mono', Menlo, Consolas, monospace;
  • Lato - licensed under the SIL Open Font License. Six weights ship preloaded.
  • Hack-ZeroSlash - MIT-licensed fork of Hack with a slashed zero, so 0 and O never trade places in code.

Rules of the road

  • Every heading uses --lh-snug (1.33); body text uses --lh-base (1.43); call-to-action or prose paragraphs at --lh-loose (1.6).
  • Monospace is reserved for metadata, code, keyboard shortcuts (<kbd>), inline tokens, and anything you want to feel terminal-native.
  • Eyebrows use text-transform: uppercase + letter-spacing: 0.05em + --fs-sm + mono. Do not inline-style - reach for the .section__eyebrow utility instead.

Foundations · 03

Spacing

Eight-step linear scale. 4 px grid, powers of two. Compose padding, gap, and block rhythm exclusively from these tokens - no arbitrary pixel values.

The scale

  • --space-0

    0 - zero

  • --space-1

    4 - hairline gap

  • --space-2

    8 - inline rhythm

  • --space-3

    12 - inside control

  • --space-4

    16 - card padding

  • --space-5

    24 - block rhythm

  • --space-6

    32 - section padding

  • --space-7

    48 - section gap

  • --space-8

    64 - page rhythm

Where each step lands

  • --space-1 (4px) - icon gap inside a chip, hairline separator, tight badge padding.
  • --space-2 (8px) - rhythm between inline elements (icon + label), badge paddings.
  • --space-3 (12px) - inside-control padding (buttons, inputs, rows).
  • --space-4 (16px) - card/panel padding, form field spacing.
  • --space-5 (24px) - block rhythm between paragraphs, card→card gap.
  • --space-6 (32px) - section internal padding.
  • --space-7 (48px) - gap between major page sections.
  • --space-8 (64px) - rhythm between distinct page regions (hero → body).

Rules

  • Compose layouts from at most three steps per level. If you need a fourth, you are probably mis-sizing a container.
  • Prefer gap over margin. Margin is for resetting content; gap is for laying out controls.
  • Never mix token-spacing with arbitrary pixel values. If a step is missing, add it to the scale.
.my-card {
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

Foundations · 04

Iconography

A curated Lucide subset. 24x24 viewBox, 2px stroke, currentColor. One canonical source, three delivery modes - React, CSS sprite, inline body.

Why Lucide

  • Permissive license (ISC).
  • 1400+ icons, one visual language (stroke-based, 24x24, consistent stroke weight).
  • Actively maintained fork of Feather with a larger, more consistent set.

We do not vendor the whole set. Each wave adds only the icons an actual component needs, keeping the bundle honest.

Curated set

Wave 2 expands the catalog to 60 Lucide glyphs across nine categories - arrows, feedback, object, identity, media, math, nav, editing, and a small miscellany (calendar, clock, mail, globe, filter, bell, eye, heart, star, bookmark, share). Every shipped component references from this set; nothing outside it should appear in product UI without a review.

  • arrow-down

  • arrow-left

  • arrow-right

  • arrow-up-down

  • arrow-up-right

  • arrow-up

  • bell

  • book

  • bookmark

  • calendar

  • check

  • chevron-down

  • chevron-left

  • chevron-right

  • chevron-up

  • circle-alert

  • circle-check

  • circle-x

  • clock

  • code

  • copy

  • download

  • equal

  • external-link

  • eye-off

  • eye

  • file-text

  • file

  • filter

  • folder

  • globe

  • grid

  • heart

  • home

  • image

  • info

  • key

  • loader

  • lock

  • log-out

  • mail

  • menu

  • minus

  • pause

  • pencil

  • play

  • plus

  • search

  • settings

  • share

  • star

  • terminal

  • trash

  • triangle-alert

  • unlock

  • upload

  • user

  • users

  • video

  • x

Usage

import { Icon } from './ui/icons/Icon';

// Decorative (default) - sits next to a text label inside a button.
<Icon name="copy" size={16} />

// Meaningful - icon stands alone and needs an accessible name.
<Icon name="external-link" label="Open in new tab" />

Pure-HTML consumers reference the CSS sprite instead:

<svg width="16" height="16" aria-hidden="true">
  <use href="/uikit/sprite.svg#fcc-icon-copy" />
</svg>

Rules

  • Use aria-hidden when an icon pairs with text; use label + role="img" when it stands alone.
  • Stick to the 24 / 20 / 16 size ladder. Inline buttons take 13–16; standalone buttons take 20; marketing/hero surfaces take 24.
  • Icons inherit colour via currentColor. Never set an explicit stroke override - theme the parent instead.

Foundations · 05

Motion

Snap-in, ease-out, 120–260ms. No bounces, no drifts, no hover parallax. Every transition has a purpose and an honest end state.

Tokens

--ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--dur-fast: 120ms;
--dur-base: 180ms;
--dur-slow: 260ms;

Feel

  • --dur-fast · --ease-snap

    120ms · enter for tooltips, menu fades

  • --dur-base · --ease-snap

    180ms · tabs, switches, state swaps

  • --dur-slow · --ease-out

    260ms · dialog, drawer, page transitions

Rules

  • Default ease is --ease-snap. Use --ease-out only for entrances (dialog, drawer, page transitions) where a longer trailing decay reads as intentional arrival.
  • Default duration is --dur-base (180ms). Micro-interactions (tooltips, hover fades) take --dur-fast. Large surfaces (dialog, drawer) take --dur-slow.
  • Exits are always 120–140ms with --ease-snap. Exits are faster than enters so the UI feels responsive and dismissable.
  • Respect reduced motion. Wrap non-essential transitions in @media (prefers-reduced-motion: reduce) { transition: none; }. State still swaps; the animation does not.
  • No bounces, no springs, no overshoot. No parallax, no hover scale > 1.02. Motion is informative, not decorative.

Per-component budget

Surface

Enter

Exit

Notes

Dialog

fade 120ms + scale 0.96 → 1 260ms –ease-out

reverse 140msBackdrop fades independently.
Drawer

translateX 260ms –ease-out

reverse 200msAlso translateY for bottom sheet.
Dropdown / ListboxtranslateY(-4) + fade 120msreverse 100msNo scale - menus should not “grow”.
Tooltipfade 80ms after 300ms delayfade 80ms, no delayDelay resets per group.
Tabs

indicator translateX 180ms –ease-snap

-Content cross-fades 120ms.
Switch

thumb translateX 160ms –ease-snap

-No bounce.
Page

opacity 120ms on Astro view transitions

-Reduced-motion honored.

Foundations · 06

Voice

Terminal-native copy. Clear, learner-respectful, never cutesy. Command-line Chic on the page - literal where it matters, warm where it counts.

Tone pillars

  • Direct. “Copy-paste and it just works.” Not “Our robust, enterprise-grade toolkit empowers you to…”
  • Literal. Say exactly what a surface does: “Toggle theme”, “Dismiss notification”, “Search components”. No marketing verbs on buttons.
  • Learner-first. Assume the reader is smart but new. Gloss acronyms on first use (“SRI - subresource integrity hashes”) and link to deeper primers.
  • Honest about tradeoffs. “Ships 3KB of vanilla JS” is better than “lightweight runtime”. Numbers earn trust.

Micro-copy patterns

Surface

Do

Don’t

Primary button

Copy, Install, Run benchmark

Click here, Submit

Secondary button

Cancel, Back, Dismiss

No thanks, Skip for now

Empty state titleNo components yetOops! Nothing to see here 😅
ErrorToken expired - sign in again.Something went wrong.
SuccessCopied to clipboard.

Done!, Success!

TooltipKeyboard shortcut: ⌘KTap to open search

The “command-line” tell

When we want to lean into the brand, frame prose as if it’s happening in a terminal:

camp@fcc:~$ curl design.freecodecamp.org/components/button.md
camp@fcc:~$ open components

Use this sparingly - the landing hero, a README banner, a CLI-adjacent guide page. Every section heading does not need to be a prompt.

Accessibility voice

  • Error messages identify what failed and how to recover in the same sentence.
  • Loading states use a noun-first structure: “Loading palette…” not “Please wait…”.
  • Confirmation dialogs frame the destructive verb in the title (“Delete component?”) and name the affected thing in the body.

Localisation guardrails

  • Keep sentences short - 60 characters or fewer for labels.
  • No idioms in UI surfaces. Idioms are fine in marketing prose, never in a button.
  • Date / time always formatted with Intl.* - never hand-rolled.

Foundations · 07

Overview

What this handbook covers, who it is for, and the design language in one breath. Read this first; the rest expands a paragraph.

What this handbook is

A reference for everyone shipping under the freeCodeCamp brand - the curriculum platform, the news site, the forum, every campaign page, every internal tool. Use it to make decisions; do not treat it as a style guide to memorize. When in doubt, run the call against the principles below and the do/don’ts at the bottom of the handbook.

Who it is for

  • Engineers building with copied UIKit source - components, layouts, full pages.
  • Designers picking colors, type, motion, and iconography for new surfaces.
  • Writers authoring micro-copy, error states, marketing prose, and curriculum text.
  • Contributors opening pull requests against any freeCodeCamp repo.

If you are an external partner co-branding with freeCodeCamp, the Brand section is the contract you must satisfy.

The design language in one breath

Command-line Chic. Square corners. Three-pixel borders. No drop shadows. Inverted-fill primaries. Two-pixel-stroke icons on a 24-pixel grid. Lato body, Hack-ZeroSlash mono. Snap easing, 180 ms default. Direct copy. Honest empty states. Accessibility before flourish.

The whole kit is a CSS custom-property layer on top of semantic markup. Recolor the palette by swapping one class on <html>. Restyle a section by overriding tokens, never by overriding classes.

Hierarchy of decisions

When two principles collide, this is the order of precedence:

  1. Accessibility. WCAG AA contrast, keyboard reach, reduced-motion respect, screen-reader semantics. Never sacrifice for aesthetics.
  2. Clarity. A learner reading at a tenth-grade level on a 5-inch phone, on transit Wi-Fi, in a second language, must understand the surface in the first second.
  3. Consistency. Same component, same behavior, same shape across surfaces. Surprises are bugs.
  4. Brand voice. Direct, literal, learner-first. No marketing fluff.
  5. Density. Honest information density. Not enterprise-cluttered, not influencer-airy.
  6. Aesthetics. Square corners, no shadows, the whole Command-line Chic. The look is the by-product of the rules above, not the goal.

If you find yourself bending #1 or #2 to satisfy #6, stop and escalate.

How to use the rest of this handbook

Each subsequent section is a short, opinionated read with do/don’ts. Pick the section that matches your decision - palette, type, spacing, iconography, motion, voice, brand - and follow the rules. The cross-cutting Do / Don’t gallery at the end catches composition mistakes that no single section can.

Foundations · 08

Brand

Logo usage, wordmark vs symbol, clearspace, sizing, and partner co-branding. The contract every external surface must satisfy before it carries the freeCodeCamp mark.

Marks

freeCodeCamp ships three marks. Use the right one for the surface.

  • Wordmark - freeCodeCamp rendered in bold weight at the local body font. Use as the primary logotype in product navigation, page headers, footers, and prose (“freeCodeCamp” sentence-cased, never “FreeCodeCamp” or “Freecodecamp”). The f is always lowercase, the C always uppercase.
  • Symbol - the campfire glyph (the abstract flame mark). Use as a favicon, app icon, social avatar, badge, or anywhere a square mark works better than text. Never embed text inside the symbol.
  • Lockup - wordmark + symbol composed horizontally. Use for splash screens, partner co-branding, certificates, and printed material where both recognition cues are needed.

The canonical assets ship in packages/uikit-css/src/brand/ (SVG + PNG, dark + light variants, plus a printable asset kit zip). Always reach for the SVG version on the web; PNG is for embed contexts that cannot consume SVG.

Clearspace

Around every mark, reserve a clearspace equal to the cap height of the wordmark (or the height of the symbol’s flame element, whichever is smaller). No other element - tagline, divider, image, second logo, decorative pattern - may enter that zone. The clearspace is the mark’s silence; it is what makes the mark legible.

On dense interfaces (chips, table cells, dense toolbars), use the symbol alone at 16-24px. The wordmark needs at least 96px of width to remain readable; below that, fall back to the symbol.

Minimum sizes

Mark

Minimum width (digital)

Minimum width (print)

Wordmark96 px24 mm
Symbol16 px4 mm
Lockup144 px36 mm

Below these floors the mark is decoration, not branding. Use a different mark or omit it.

Color rules

The wordmark and lockup ship in two variants - light (white on dark surface) and dark (gray-90 on light surface). Use whichever provides ≥4.5:1 contrast against the background. Never recolor the mark to match a campaign palette; the brand stays steady so campaign palettes can move.

The symbol may carry the gold accent (--cta-background) when used as a favicon, app icon, or single-color mark on a neutral surface. It may not carry the danger, warning, or info palettes - those are reserved for state, not identity.

On photographic backgrounds, place the mark on a solid surface-two block (var(--background-secondary)) before composing. Direct overlay on photography is forbidden - contrast is unreliable and the mark loses its silence.

Logo Do / Don’ts

  • Do keep clearspace clear. Don’t crop a hero image into the clearspace zone.
  • Do use the canonical SVG. Don’t rasterize and re-rasterize through screenshots.
  • Do scale the lockup proportionally. Don’t stretch the wordmark or compress the symbol.
  • Do use the dark variant on light surfaces. Don’t invert the gold accent for “more pop”.
  • Do pair the symbol with the wordmark for first impressions. Don’t introduce a tagline inside the lockup; taglines are a separate type element.
  • Do request legal review for any partner co-brand. Don’t add a partner mark to the lockup without sign-off.

Partner co-branding

When freeCodeCamp appears alongside a partner mark (course sponsors, certification employers, university programs), follow this composition:

  1. Partner mark on the left, freeCodeCamp lockup on the right.
  2. A vertical divider (1px, var(--border-strong)) separates the two, with the divider’s height equal to the taller mark’s cap height.
  3. Both marks share a single horizontal baseline. Optical-center the smaller mark if needed.
  4. Equal clearspace on both outsides.
  5. The two marks must reach the same x-height (visual weight, not absolute height - partner marks vary in proportion).

Partner co-branding requires written approval from the freeCodeCamp brand team for any public-facing surface. Internal pilot pages are exempt; once a co-brand goes public, it goes through review.

What is not the brand

  • Curriculum content - built on the brand but governed by curriculum guidelines (separate doc).
  • Forum threads - community voice, not freeCodeCamp voice. The forum carries the brand chrome but its text is owned by users.
  • Community-created content - tutorials, articles, YouTube channels. These can describe freeCodeCamp but must not present as freeCodeCamp.
  • Legacy marks - the pre-2022 campfire variant, the all-caps wordmark, any teal/lime palette artifact. Replace on contact.

When in doubt, the rule is conservative: the brand exists so a learner can recognize freeCodeCamp on any surface, in any country, in any context. Anything that confuses that recognition is wrong, however nice it looks.

Marks

Three official marks. The dotted frame is the clear-space envelope - nothing else enters that margin.

  • freeCodeCamp Primary mark

    Primary

    Full lockup - wordmark + puck. Default for hero + header.

    Download SVG
  • freeCodeCamp Secondary mark

    Secondary

    Compact lockup - wordmark only. Navs, cards, footers.

    Download SVG
  • freeCodeCamp Puck mark

    Puck

    Standalone puck. Favicons, avatars, tight chrome.

    Download SVG

Clear space

The puck height x is the minimum margin on every side. Drag the slider to tighten the margin - the frame turns red when it falls below 0.5x.

freeCodeCamp primary mark

Misuse

Three failure modes. Don't stretch, don't recolour, don't sit on a busy background.

Do

Primary mark, correct proportions

Keep aspect ratio, one-tone fill, quiet surface.

Don't

Stretched mark

Don't stretch - aspect ratio is locked.

Recoloured mark

Don't recolour - use the kit palette.

Mark on busy background

Don't sit on a busy background - find a calm surface.

Wordmark usage

One canonical spelling, one compact abbreviation, one graphic-only fallback.

  • freeCodeCamp

    Full name. Long-form copy, first use in body text, legal, about pages. Lowercase f, capital C and C.

  • fCC

    Short form. Breadcrumbs, tight UI chrome, code comments. Only after a full-name first use on the same surface.

  • freeCodeCamp puck

    Puck-only. Favicons, avatars, 32 px or smaller. Never as the sole brand cue in a header.

Asset kit

The three marks as a single zip. Brand PDF + Figma community file arrive post-1.0.

Download asset kit

Foundations · 09

Do / Don't

Cross-cutting visual rules that no single foundation captures. Composition, contrast, density, voice - caught here so they cannot leak into shipping pages.

Composition

  • Do anchor primary actions on the bottom-right of forms and modals; the eye lands there last on left-to-right scans. Don’t float a CTA in the middle of a form - the surrounding fields will compete for attention.
  • Do use the eight-step spacing scale exclusively. Don’t ship padding: 13px because “it looks better” - open the inspector and find the token that solves it.
  • Do group related controls inside a <Fieldset> with a real legend. Don’t rely on visual proximity alone; assistive tech needs the structural hint.
  • Do keep one primary CTA per visible viewport. Don’t ship two <Button variant='cta'> in the same fold; the second cancels the first.

Contrast

  • Do check every text-on-surface pair against WCAG AA (4.5:1 body, 3:1 large). Don’t assume token combinations are always safe - --foreground-tertiary on --background-secondary is a known borderline pair on the dark palette.
  • Do show focus rings with ≥3:1 contrast against both the focused element and its surrounding surface. Don’t rely on the browser default outline; <Button> and friends ship explicit :focus-visible rules.
  • Do carry information in shape, label, and color together. Don’t encode meaning in color alone; learners with deuteranopia will lose the signal.

Density

  • Do lean dense in tooling surfaces (dashboards, data tables, admin panels) - the audience is professional and visiting often. Don’t lean airy where dense reads as scaffolding gaps.
  • Do lean spacious in marketing and curriculum surfaces - the audience is reading deeply for the first time. Don’t copy the dashboard density into the news site; it reads as cluttered.
  • Do truncate honestly. Don’t truncate without an affordance - every needs a hover, click, or aria-expanded to recover the full string.

Voice

  • Do name the action on the button verb-first (“Run tests”, “Save draft”). Don’t ship “Submit” or “OK” - those are placeholder verbs.
  • Do write error messages that name the cause and the fix (“Token expired - sign in again.”). Don’t ship “Something went wrong”; learners blame themselves.
  • Do use sentence case in product UI (Save changes). Don’t use Title Case (Save Changes) outside of marketing splash heroes.
  • Do let empty states recommend an action (“No certifications yet - pick a track.”). Don’t apologize on behalf of the product (“Oops! Nothing here yet 😅”).

Iconography

  • Do pair every icon-only button with an aria-label. Don’t assume meaning from the glyph alone - the meaning lives in the label.
  • Do keep the 24x24 viewBox + 2px stroke. Don’t mix Lucide with another icon set; the stroke weight will fight.
  • Do color icons with currentColor so they inherit text tone. Don’t hardcode a hex on an SVG; it will not re-theme.

Motion

  • Do wrap any non-essential transition in prefers-reduced-motion. Don’t ship a parallax hero, ever - the brand has none.
  • Do keep exits faster than enters (120–140 ms). Don’t mirror enter timing on exit; a slow dismiss feels stuck.
  • Do snap to the end state on interaction. Don’t introduce overshoot or bounce; the kit has zero springs.

Brand

  • Do reach for the canonical SVG marks in packages/uikit-css/src/brand/. Don’t screenshot the wordmark off a webpage and re-embed.
  • Do maintain the clearspace around every mark. Don’t crop hero imagery into the clearspace zone.
  • Do request review for partner co-branding. Don’t add a third logo to the lockup without sign-off.

When you find an exception

If you find yourself bending one of these rules, write it down in the pull request description. Brand and design review can sign off on the exception, but the friction is intentional - the rules exist because the kit’s recognizability depends on them. Two surfaces breaking the same rule is no longer an exception; it is a rule change. Open an issue against this handbook so the rule moves with the practice.

Install

Copy the source, own the code

UIKit is a copy-source registry, shadcn-style: components are copied into your project and tailored there - nothing installs from npm. Every page under/components/<slug>.md carries install steps, props, and the full source.

1 · Install the theme (once)

Copy tokens.css + base.css from/registry/theme.md into src/ui/theme/, then import them - tokens first. Fonts are listed in thestarter guide.

@import './ui/theme/tokens.css';
@import './ui/theme/base.css';

2 · Copy a component

Grab the files from the component page (or the raw URLs it lists) and import its CSS once, globally.

# design.freecodecamp.org/components/button.md
src/ui/button/Button.tsx
src/ui/button/button.css

3 · Use it - it is your code now

import { Button } from './ui/button/Button';

<Button variant="cta">Start curriculum</Button>

Tailor the component freely. Recolour by editing token values in your copied tokens.css - keep the token names, and never hard-code colors in component CSS.

For coding agents

Point agents at /llms.txt (index), /registry/index.json (machine-readable manifest), or/registry/starter.md (bootstrap guide with a ready-made AGENTS.md snippet).

Bundle

Download & self-host the bundle

design.freecodecamp.org is not a CDN - do not hotlink assets from it. Download the bundle once and serve it from your own host. One stylesheet carries every token, every component class, and every @font-face. Font URLs are relative (./fonts/*), so keep thefonts/ folder beside the stylesheet.

1. Download once (styles + fonts)

curl -O https://design.freecodecamp.org/cdn/styles.min.css
# fonts referenced by the bundle (relative ./fonts/*):
mkdir -p fonts && (cd fonts \
  && curl -O https://design.freecodecamp.org/cdn/fonts/Lato-Regular.woff \
  && curl -O https://design.freecodecamp.org/cdn/fonts/Hack-ZeroSlash-Regular.woff2)
# full file list + SRI hashes: https://design.freecodecamp.org/cdn/manifest.json

2. Reference your own copy

<link rel="stylesheet" href="/styles.min.css" />

The design.freecodecamp.org bundle is rolling, not version-pinned - it always reflects the current main. Your downloaded copy is your pin; re-download to update.

Tailwind

Use with Tailwind

The Tailwind preset is a thin layer that translates every UIKit token into a Tailwind utility prefix. bg-cta-background reads the same CSS custom property the BEM classes do, so palette switches stay synchronized across both surfaces.

1 · Install

Install Tailwind, then copy preset.ts +plugin.ts from/registry/tailwind.md into src/ui/tailwind/.

pnpm add tailwindcss

2 · Wire it up

// tailwind.config.js
import fcc from './src/ui/tailwind/preset';

export default {
  presets: [fcc],
  content: ['./src/**/*.{ts,tsx,astro,html}'],
  theme: { extend: {} }
};
/* src/index.css */
@import 'tailwindcss';
@import './ui/theme/tokens.css';

3 · Pick a pattern

Don't double-import utility surfaces. Pick one of these two patterns:

  1. Tailwind utility-first - importtailwindcss + the copied tokens only; let the preset surface the tokens. Skip the component CSS entirely.
  2. BEM-first + Tailwind escape hatch - import both, but order the copied component CSS beforetailwindcss so utility classes win the cascade.

The shipped docs site uses (2): BEM ships as the canonical surface in the playground, and the preset is the way to drop into Tailwind for project-specific work.

Palette switching

The preset reads CSS custom properties at runtime, so swapping<html class="dark-palette"> forlight-palette instantly recolors everybg-*, text-*, andborder-* utility. No build step.

Recipes

Compose components

The playground shows each component in isolation. These recipes collect the patterns that compose two or more.

Form composition

import { FormGroup } from './ui/form-group/FormGroup';
import { Input } from './ui/input/Input';
import { HelpBlock } from './ui/help-block/HelpBlock';
import { Button } from './ui/button/Button';

export function NewsletterForm() {
  return (
    <form onSubmit={onSubmit}>
      <FormGroup>
        <label htmlFor='email'>Email address</label>
        <Input id='email' type='email' required />
        <HelpBlock>One curriculum update per week.</HelpBlock>
      </FormGroup>
      <Button type='submit' variant='cta'>Subscribe</Button>
    </form>
  );
}

Modal from a button

import { useState } from 'react';
import { Modal } from './ui/modal/Modal';
import { Button } from './ui/button/Button';

export function ResetCta() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button variant='danger' onClick={() => setOpen(true)}>
        Reset progress
      </Button>
      <Modal open={open} onClose={() => setOpen(false)} title='Reset progress?'>
        <Modal.Body>You will lose 17 of 32 steps.</Modal.Body>
        <Modal.Footer>
          <Button onClick={() => setOpen(false)}>Cancel</Button>
          <Button variant='danger' onClick={confirmReset}>Confirm</Button>
        </Modal.Footer>
      </Modal>
    </>
  );
}

Toast on action

import { useToast } from './ui/toast/Toast';
import { Button } from './ui/button/Button';

export function SaveButton() {
  const toast = useToast();
  return (
    <Button
      onClick={() => {
        save();
        toast.create({
          title: 'Saved',
          description: 'Your progress is synced.',
          variant: 'success'
        });
      }}
    >
      Save
    </Button>
  );
}

Sidebar layout with navbar

import { SidebarLayout } from './ui/sidebar-layout/SidebarLayout';
import { Navbar } from './ui/navbar/Navbar';
import { Sidebar } from './ui/sidebar/Sidebar';

export function Shell({ children }) {
  return (
    <SidebarLayout
      header={<Navbar start={<Brand />} end={<UserMenu />} />}
      aside={<Sidebar sections={navSections} />}
    >
      {children}
    </SidebarLayout>
  );
}

Contributing

Contribute a change

UIKit lives in the freeCodeCamp/UIkit monorepo. Small fixes go straight to PR; larger features start with a short RFC in a GitHub Discussion.

  1. Fork the repo and branch off main.
  2. One concern per commit. TDD: failing test → implementation → green.
  3. Commit with type(scope): subject - title-only, no body. Use feat, fix, refactor, chore, docs, or test.
  4. Run the full battery locally before pushing:
    pnpm install
    pnpm test
    pnpm build
    pnpm format:check
    pnpm --filter @freecodecamp/uikit-docs test:visual
  5. Open a PR against main. CodeRabbit reviews; CI is the source of truth. Merges to main deploy straight todesign.freecodecamp.org - no separate release step.