Skip to content
Back to Build Tutorials

Component Generation

Scaffold new UI components with the right structure, tests, and Storybook stories from a template.

Why

Every component in a design system should have the same structure: props interface, forwardRef, a Storybook story with all variants, and a unit test. Hand-creating these 4 files for every new component wastes 15 min and introduces inconsistency — some components get tests, some don't, some use forwardRef, some forget. A generator (plop, hygen, or a custom script) produces all 4 files from a template in 5 seconds, enforcing the same structure every time. This is how design systems scale to 100+ components without becoming a maintenance nightmare: the structure is uniform, so onboarding to a new component is zero-effort.

When

When you have ≥10 components or a design system. Takes 1 h to set up plop. Skip for one-off components.

How

1. Install plop and create a plopfile with a component generator
   - Prompts: component name, directory (atoms/molecules/organisms), hasVariants
   - Templates: Component.tsx, Component.test.tsx, Component.stories.tsx, index.ts

2. Template the Component.tsx with the standard structure
   - React.forwardRef, typed props interface, className merge with cn()
   - Export as named + default; include a displayName for DevTools

3. Template the Storybook story with at least: default, with className, disabled
   - Use args composition so variants inherit base args
   - Add a docsOnly story for documentation without rendering

4. Template the test with a render + snapshot + a behavior test
   - render(<Component />) should not throw
   - Test the key interaction (click, input) — not just that it renders

5. Wire into package.json: "gen:component": "plop component"
   - Run it, answer 3 prompts, get 4 files that all pass lint+test on first try

Example

// plopfile.ts
export default function (plop) {
  plop.setGenerator("component", {
    prompts: [
      { type: "input", name: "name", message: "Component name (PascalCase)" },
      { type: "list", name: "level", choices: ["atoms", "molecules", "organisms"] },
    ],
    actions: [
      { type: "add", path: "src/components/{{level}}/{{name}}/{{name}}.tsx", templateFile: "templates/Component.tsx.hbs" },
      { type: "add", path: "src/components/{{level}}/{{name}}/{{name}}.test.tsx", templateFile: "templates/Component.test.tsx.hbs" },
      { type: "add", path: "src/components/{{level}}/{{name}}/{{name}}.stories.tsx", templateFile: "templates/Component.stories.tsx.hbs" },
    ],
  });
}

// templates/Component.tsx.hbs
import { forwardRef } from "react";
export interface {{name}}Props { className?: string; }
export const {{name}} = forwardRef<HTMLDivElement, {{name}}Props>(function {{name}}({ className }, ref) {
  return <div ref={ref} className={className} />;
});
{{name}}.displayName = "{{name}}";

Common Problems

  • ⚠Templates that drift from the actual project conventions, so generated code needs manual fixing every time.
  • ⚠Generating only the .tsx file without tests or stories, so the component exists but is undocumented and untested.
  • ⚠Not handling compound components (Select + Option) where one generator should produce multiple related files.
  • ⚠Forgetting to update the barrel export (index.ts) after generating, so the component isn't importable.

Best Practices

  • ✓Keep the templates in the repo and review changes to them like any other code — they generate code, so they're high-leverage.
  • ✓Add a post-generation step that runs lint+test on the new files and fails if they don't pass — catches template drift.
  • ✓Support both simple and compound components in one generator with a 'compound' prompt that adds sub-files.
  • ✓Generate the Storybook story with autodocs enabled so the component is documented from day one.