Build a searchable component registry that helps teams find, compare, and understand design system components — with metadata, usage examples, version history, and Figma links.
Build a searchable component registry that helps teams find, compare, and understand design system components — with metadata, usage examples, version history, and Figma links.
A well-designed metadata schema makes the registry queryable and auto-completeable. Define it as a TypeScript interface that each component's metadata file must satisfy:
1// packages/registry/src/types.ts
2type ComponentStatus = 'stable' | 'beta' | 'deprecated' | 'experimental';
3type Platform = 'web' | 'ios' | 'android' | 'react-native';
4type Category = 'actions' | 'inputs' | 'navigation' | 'feedback' | 'layout' | 'data-display';
5
6interface ComponentMetadata {
7 /** Machine-readable identifier */
8 id: string;
9 /** Display name */
10 name: string;
11 /** One-sentence description */
12 description: string;
13 status: ComponentStatus;
14 category: Category;
15 platforms: Platform[];
16 /** Package version when component was introduced */
17 since: string;
18 /** Who owns this component (core team or squad) */
19 owner: string;
20 /** Figma component URL */
21 figmaUrl?: string;
22 /** GitHub source file path */
23 sourcePath: string;
24 /** Storybook story URL */
25 storybookUrl: string;
26 /** Whether this component is RSC-compatible */
27 isServerCompatible: boolean;
28 /** WCAG compliance level */
29 a11yLevel: 'AA' | 'AAA' | 'partial' | 'none';
30 /** Related components */
31 relatedComponents: string[];
32 /** Tags for search */
33 tags: string[];
34}Store metadata as colocated JSON files next to each component: src/Button/Button.registry.json. This makes metadata visible in code review and version-controlled alongside the component.
Auto-generate the base registry entry from component source to reduce manual maintenance:
1// scripts/generate-registry.ts
2import { Project, SyntaxKind } from 'ts-morph';
3import { writeFileSync } from 'fs';
4import { glob } from 'glob';
5
6async function generateRegistryEntries() {
7 const project = new Project({ tsConfigFilePath: 'tsconfig.json' });
8 const files = await glob('src/**/*.tsx', { ignore: ['**/*.test.*', '**/*.stories.*'] });
9
10 for (const filePath of files) {
11 const sourceFile = project.addSourceFileAtPath(filePath);
12
13 // Find exported component interfaces
14 const interfaces = sourceFile.getInterfaces()
15 .filter(i => i.isExported() && i.getName().endsWith('Props'));
16
17 for (const iface of interfaces) {
18 const componentName = iface.getName().replace('Props', '');
19 const props = iface.getProperties().map(p => ({
20 name: p.getName(),
21 type: p.getType().getText(),
22 required: !p.hasQuestionToken(),
23 docs: p.getJsDocs().map(d => d.getDescription()).join(' '),
24 }));
25
26 const registryEntry = {
27 id: componentName.toLowerCase(),
28 name: componentName,
29 sourcePath: filePath,
30 props,
31 // Fields that need manual completion:
32 description: '<!-- TODO: Add description -->',
33 status: 'stable',
34 category: '<!-- TODO: Set category -->',
35 };
36
37 writeFileSync(
38 filePath.replace('.tsx', '.registry.json'),
39 JSON.stringify(registryEntry, null, 2)
40 );
41 }
42 }
43}Run this script when adding a new component. It scaffolds the registry JSON with all props auto-extracted, leaving only the semantic fields (description, category, Figma URL) for a human to complete.
Build the registry as a searchable page in your design system site or Storybook:
1// apps/docs/src/app/components/page.tsx (Next.js)
2import { registry } from '@acme/registry';
3
4export default function ComponentsPage() {
5 return (
6 <div className="registry-page">
7 <header>
8 <h1>Component Registry</h1>
9 <p>{registry.length} components across {platforms.join(', ')}</p>
10 </header>
11
12 <div className="registry-filters">
13 <input
14 type="search"
15 placeholder="Search components..."
16 aria-label="Search component registry"
17 />
18 <fieldset>
19 <legend>Status</legend>
20 {['stable', 'beta', 'experimental', 'deprecated'].map(status => (
21 <label key={status}>
22 <input type="checkbox" value={status} />
23 <StatusBadge status={status} />
24 </label>
25 ))}
26 </fieldset>
27 <fieldset>
28 <legend>Category</legend>
29 {categories.map(cat => (
30 <label key={cat}>
31 <input type="checkbox" value={cat} />
32 {cat}
33 </label>
34 ))}
35 </fieldset>
36 </div>
37
38 <div className="registry-grid">
39 {filteredComponents.map(component => (
40 <ComponentCard key={component.id} component={component} />
41 ))}
42 </div>
43 </div>
44 );
45}A clear status taxonomy prevents teams from depending on unstable components. Define each status precisely and make it visible everywhere:
1// Status definitions and visual treatment
2const STATUS_CONFIG = {
3 stable: {
4 label: 'Stable',
5 description: 'Production-ready. API is frozen until next major version. Safe for all use cases.',
6 color: '#16a34a',
7 badge: 'green',
8 canUseInProduction: true,
9 },
10 beta: {
11 label: 'Beta',
12 description: 'API may change in minor versions. Use in production with awareness of migration cost.',
13 color: '#d97706',
14 badge: 'yellow',
15 canUseInProduction: true,
16 },
17 experimental: {
18 label: 'Experimental',
19 description: 'May be removed or completely redesigned. Do not use in production.',
20 color: '#7c3aed',
21 badge: 'purple',
22 canUseInProduction: false,
23 },
24 deprecated: {
25 label: 'Deprecated',
26 description: 'Will be removed in next major version. Migrate to the replacement immediately.',
27 color: '#dc2626',
28 badge: 'red',
29 canUseInProduction: false,
30 },
31} as const;
32
33// StatusBadge component for Storybook and registry
34function StatusBadge({ status }: { status: keyof typeof STATUS_CONFIG }) {
35 const config = STATUS_CONFIG[status];
36 return (
37 <span
38 data-status={status}
39 role="img"
40 aria-label={`Component status: ${config.label}`}
41 title={config.description}
42 style={{ color: config.color }}
43 >
44 {config.label}
45 </span>
46 );
47}Show the StatusBadge in the Storybook sidebar, on the component's documentation page, in registry search results, and in the component's TypeScript JSDoc. Everywhere a team encounters the component, the status is visible.
The registry's most practical feature is the trilink: every component has direct links to its Figma component, GitHub source file, and Storybook story — all in one place. Implement this as a consistent header on every component's registry card:
1function ComponentLinks({ component }: { component: ComponentMetadata }) {
2 return (
3 <nav aria-label={`Links for ${component.name}`} className="component-links">
4 {component.figmaUrl && (
5 <a
6 href={component.figmaUrl}
7 target="_blank"
8 rel="noopener noreferrer"
9 className="link-figma"
10 >
11 <FigmaIcon aria-hidden />
12 Figma
13 </a>
14 )}
15 <a
16 href={`https://github.com/acme/design-system/blob/main/${component.sourcePath}`}
17 target="_blank"
18 rel="noopener noreferrer"
19 className="link-github"
20 >
21 <GitHubIcon aria-hidden />
22 Source
23 </a>
24 <a
25 href={component.storybookUrl}
26 className="link-storybook"
27 >
28 <StorybookIcon aria-hidden />
29 Storybook
30 </a>
31 </nav>
32 );
33}Automate the Figma URL population using the Figma REST API: after publishing a component to Figma, a GitHub Action fetches the component node ID and updates the registry JSON automatically. This prevents Figma links from going stale as the Figma file structure changes.
Create a ComponentMetadata TypeScript interface and write registry JSON files for your top 10 most-used components. Build a simple searchable HTML page that renders them — that is your first component registry.
Your progress is saved in this browser, on this device only.