Skip to content

Component Story

nib component story <name> generates a .stories.ts scaffold from a component contract — wiring up variant controls, size controls, state stories, and a11y metadata automatically. You replace one PLACEHOLDER import and the story works.


Before and After

Before: Writing a Button story means manually reading the contract JSON, listing all variants, setting up argTypes for each control, and writing the a11y description. 20–30 minutes per component.

After: Run one command. All variants, sizes, and states are wired. The a11y description (role, keyboard patterns, ARIA attributes) is pulled directly from the contract. Replace the import and you're done.


Quick Start

sh
# Generate a story for the Button component
nib component story Button

# Overwrite an existing story
nib component story Button --overwrite

# Override framework detection
nib component story Button --framework vue3

# Write to a custom path
nib component story Button --output src/ui/Button.stories.ts

What Gets Generated

Given a Button.contract.json with variants primary, secondary, ghost, danger and states default, hover, focus, disabled, loading — the generated story includes:

  • Framework import — auto-detected from package.json (@storybook/react-vite, @storybook/vue3-vite, etc.)
  • Component anatomy — JSDoc comment listing all anatomy parts from the contract
  • PLACEHOLDER import — one line to replace with your real component
  • Metatitle, component, tags: ["autodocs"], parameters.docs.description pre-filled from contract a11y metadata
  • argTypesvariant select, size select, disabled boolean, loading boolean
  • Variant stories — one exported story per variant (Primary, Secondary, Ghost, Danger)
  • Size stories — one exported story per size (Small, Medium, Large)
  • State stories — one exported story per non-default state (Hover, Focus, Disabled, Loading)
  • AllVariants — overview story with commented example of all variants side by side

Example Output

ts
/**
 * Button stories — generated by nib from .nib/components/Button.contract.json
 * Run `nib component story Button --overwrite` to regenerate.
 *
 * NEXT STEPS:
 * 1. Replace the PLACEHOLDER import below with your real component import.
 * 2. Fill in the render function in AllVariants.
 * 3. Remove this comment block.
 */

import type { Meta, StoryObj } from "@storybook/react-vite";

/**
 * Component anatomy:
 * - root: the button element
 * - icon: optional leading icon
 * - label: button text
 * - spinner: loading indicator
 */

// PLACEHOLDER: Replace with your real component import.
// Example: import { Button } from "../components/Button";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Button = (_props: any): null => null;

const meta = {
  title: "Components/Button",
  component: Button,
  tags: ["autodocs"],
  parameters: {
    layout: "centered",
    docs: {
      description: {
        component: "Interactive button. WCAG role: button. Keyboard: Enter — activate. Space — activate. ARIA attributes: aria-disabled, aria-busy.",
      },
    },
  },
  argTypes: {
    variant: {
      control: "select",
      options: ["primary", "secondary", "ghost", "danger"],
      description: "primary: Main call-to-action; secondary: ...",
      table: { defaultValue: { summary: "primary" } },
    },
    disabled: { control: "boolean", description: "Disabled state" },
    loading: { control: "boolean", description: "Loading state" },
  },
  args: {
    variant: "primary",
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// --- Variant stories ---

export const Primary: Story = {
  name: "Primary — Main call-to-action",
  args: { variant: "primary" },
};

export const Secondary: Story = {
  name: "Secondary — ...",
  args: { variant: "secondary" },
};

// --- State stories ---

export const Disabled: Story = {
  args: {
    variant: "primary",
    disabled: true,
  },
};

export const Loading: Story = {
  args: {
    variant: "primary",
    loading: true,
  },
};

// --- Overview ---

export const AllVariants: Story = {
  render: (_args) => (
    // PLACEHOLDER: render all variants side by side
    // Example (React):
    // <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
    //   <Button variant="primary" />
    //   <Button variant="secondary" />
    //   <Button variant="ghost" />
    //   <Button variant="danger" />
    // </div>
    null
  ),
};

Replacing the PLACEHOLDER

The only manual step. Find this line and replace it:

ts
// PLACEHOLDER: Replace with your real component import.
// Example: import { Button } from "../components/Button";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Button = (_props: any): null => null;

Replace with:

ts
import { Button } from "../components/Button";

Or with your project's actual import path. The rest of the story works without changes.


Framework Detection

nib reads package.json to detect which Storybook framework to import from:

Dependency in package.jsonImport
react@storybook/react-vite
vue@storybook/vue3-vite
svelte@storybook/svelte-vite
lit@storybook/web-components-vite
none detectedPLACEHOLDER comment + @storybook/react-vite fallback

Override with --framework:

sh
nib component story Button --framework vue3
nib component story Button --framework svelte
nib component story Button --framework web-components

Regenerating Stories

Stories are not overwritten by default. Use --overwrite to refresh after updating a contract:

sh
# Update contract, then regenerate
nib component init Button --overwrite   # or edit the JSON directly
nib component story Button --overwrite

Contract-first

The story is derived from the contract — not the other way around. If you edit the contract and forget to regenerate the story, the story will drift. Add nib component story <name> --overwrite to your contract update workflow.


Workflow: Contract → Story → Component

The intended workflow for adding a new component:

sh
# 1. Define the component contract
nib component init MyComponent

# 2. Generate the story scaffold
nib component story MyComponent

# 3. Wire up Storybook (first time only)
nib storybook init

# 4. Start Storybook and replace the PLACEHOLDER import
npx storybook dev

Available Contracts

sh
# List all contracts in .nib/components/
ls .nib/components/*.contract.json

# Or use component list
nib component list

If no contract exists for the name you specify, nib will show available contracts:

✗ No contract found for "Card".
  Available contracts: Alert, Badge, Button, Checkbox, Dialog, Radio, Switch, TextInput, Toast

Released under the AGPL-3.0 License.