Thank you for contributing! This guide explains how to add or modify components, submit changes, and meet the quality bar required for the design system to remain consistent, accessible, and maintainable across UNDP's digital products.
- Getting Started
- Component Lifecycle
- Adding a New Component
- Story Quality Standards
- Accessibility Checklist
- Naming & API Stability
- Token Contribution
- Deprecation Policy
- Pull Request Process
- CI Gates
git clone https://github.com/undp/design-system.git
cd design-system
npm install
# Run Storybook locally
npm run storybook
# Build production assets
npm run build
# Lint JS and SCSS
npm run lint
# Smoke-test the build output
npm run smoke-test
# Check bundle sizes are within budget
npm run check-bundle-sizeEvery component carries one of four lifecycle statuses that controls how it is documented, tested, and versioned.
| Status | Meaning | Production use? |
|---|---|---|
| draft | Under active design/development. API may break between commits. | No |
| beta | Feature-complete and reviewed. Minor API tweaks still possible. | With caution |
| stable | Production-ready. API is stable within a semver major version. | Yes |
| deprecated | Replaced by a newer component or pattern. Will be removed in the next major release. | Migrate away |
Add a JSDoc @status tag to the story file's default export comment:
/**
* @status beta
*/
export default {
title: 'Components/UI components/MyComponent',
…
};The scripts/generate-component-metadata.js script reads this tag and publishes
it in docs/component-metadata.json for tooling and consumers.
Follow these steps to add a new component. Use the provided template:
.storybook/story-template.jsx
stories/
└── Components/
└── <Category>/
└── <ComponentName>/
├── <ComponentName>.jsx # React component
├── <ComponentName>.scss # Component styles
├── <ComponentName>Utils.js # Locale/data helpers
└── <ComponentName>.stories.js
If the component requires JavaScript behaviour, add a corresponding file to:
stories/assets/js/<component-name>.js
And register it in alphabetical order in both:
stories/assets/js/init.js(production initialiser)stories/assets/js/storybook-init.js(Storybook lazy-initialiser)
titlefollowing the existing taxonomy
e.g.Components/Navigation components/BreadcrumbsargTypeswithname,control,description, andtable.categoryfor each consumer-facing propcolorThemearg (inline-radio,['light', 'dark'])parameters.docs.page— inline JSX documentation (see template)- At least a
Default(light) story and aDarkstory - CDN CSS and JS references in the documentation section
To maintain consistency across the system, every component story should include the following stories (or a justification for omitting them):
| Story name | Required? | Notes |
|---|---|---|
| Default (light) | ✅ | |
| Dark | ✅ | colorTheme: 'dark' |
| RTL | ✅ if text | Set dir="rtl" on the wrapper |
| States (hover, focus, disabled, error) | ✅ where applicable | |
| Edge cases | 🟡 where applicable | Long text, empty state, many items |
| Locale variants | 🟡 where applicable | Use existing locale toolbar |
All stories are automatically captured by Chromatic in multiple viewport modes
(small, medium, large, xlarge, hd). Ensure that:
- Animations settle within 1 500 ms or use
pauseAnimationAtEnd: true - No random content (timestamps, random IDs) that would cause false positives
- Dark-mode variant is explicitly tested
Before submitting a component, verify:
- Uses semantic HTML elements (
nav,button,ul, etc.) - All interactive elements are reachable via
Tab/Shift-Tab -
Enter/Spaceactivates buttons and links -
Escapecloses any overlay, modal, or expanded state - Focus ring is visible and not suppressed without an equivalent
-
aria-expanded,aria-controls,aria-currentapplied where appropriate - Color contrast meets WCAG AA (4.5:1 for text, 3:1 for UI elements)
- Images have meaningful
alttext or are markedalt="" - Form inputs are associated with
<label>oraria-label - Component passes
@storybook/addon-a11ywith zero violations
- Use BEM-like naming consistent with existing components.
- Stable components guarantee no removals or renames within a semver major.
- Deprecated classes must be kept for one additional major release with a console warning and documented migration path.
- Component functions follow the pattern:
<camelCaseName>(element?, options?). - The
data-undpds-componentattribute key must not be renamed without a deprecation period. - New optional parameters must not change positional arguments of existing ones.
- Token names follow the pattern
$category-[sub]-name(SASS) /--undpds-category-[sub]-name(CSS). - Primitive tokens (e.g.
$color-blue-600) are the stable foundation; semantic tokens (e.g.$color-brand) are the preferred API for component styling. - Removing or renaming a primitive token is a breaking change and requires a major version bump with a migration guide.
Design tokens live in figma-tokens/input/tokens.json and are
the single source of truth for all design values.
- Export updated tokens from Figma (using the Figma Variables / Tokens plugin).
- Update
figma-tokens/input/tokens.json. - Run locally to verify:
npm run transform-tokens && npm run build. - Open a PR — the
sync-figma-tokensworkflow will generate a token changelog atfigma-tokens/CHANGELOG.md. - Review the changelog for breaking removals (🔴) before merging.
- Use
camelCasefor Figma token group names; the transform script converts them tokebab-casefor SASS/CSS. - Do not delete a primitive token without first creating a backwards- compatibility alias (the transform script does this automatically where possible, but explicit aliases may be required for complex cases).
- Semantic tokens must always reference a primitive token — never a hard-coded value.
When a component, CSS class, JS API, or token must be removed:
- Announce — document the deprecation in the current release notes and
add a
@status deprecatedtag to the story. - Keep — retain the deprecated item for at least one additional major
release, emitting a
console.warnfrom the JS initialiser where feasible. - Guide — provide a migration note explaining what to use instead:
- In the story documentation
- In
CHANGELOG.md(breaking-changes section) - In
docs/component-metadata.json(via the storyparameters.deprecationNote)
- Remove — remove the item in the next major version after the grace period, noting the removal in the changelog.
- Branch from
develop. - Ensure
npm run lintpasses with no errors. - Ensure
npm run buildsucceeds. - Ensure
npm run smoke-testpasses. - Ensure
npm run check-bundle-sizepasses (or justify the increase). - Ensure all new/changed stories pass the
@storybook/addon-a11ypanel. - Open a PR against
develop— the CI workflow runs all checks automatically. - At least one maintainer must approve before merging.
The following checks run automatically on every pull request and must pass:
| Check | Command |
|---|---|
| ESLint (JS) | npm run lint:js |
| Stylelint (SCSS) | npm run lint:css |
| Webpack build | npm run build |
| Build smoke tests | npm run smoke-test |
| Bundle size budgets | npm run check-bundle-size |
| Visual regression | Chromatic (on push) |
Chromatic visual tests run on every push; the PR author is responsible for reviewing and accepting UI changes in the Chromatic interface.