Adding Components

File Structure

packages/ui/src/components/MyComponent/
├── MyComponent.tsx          # Main implementation
├── MyComponent.stories.tsx # Storybook stories
├── index.ts               # Barrel export
└── myComponentVariants.ts # CVA variants

Step 1: Define Variants

Use class-variance-authority (CVA) for variant management.

// myComponentVariants.ts
import { cva } from "class-variance-authority";

export const myComponentVariants = cva("base classes here", {
  variants: {
    variant: {
      primary: "bg-primary text-primary-foreground",
      secondary: "bg-secondary text-secondary-foreground",
    },
    size: {
      small: "h-8 px-3 text-xs",
      medium: "h-10 px-4",
      large: "h-12 px-6",
    },
  },
  defaultVariants: {
    variant: "primary",
    size: "medium",
  },
});

Step 2: Build the Component

// MyComponent.tsx
import type { VariantProps } from "class-variance-authority";
import type { ComponentProps } from "react";
import { cn } from "../../utils/cn.js";
import { myComponentVariants } from "./myComponentVariants.js";

interface MyComponentProps
  extends ComponentProps<"button">, VariantProps<typeof myComponentVariants> {}

function MyComponent({ ref, className, variant, size, ...props }: MyComponentProps) {
  return (
    <button
      className={cn(myComponentVariants({ variant, size, className }))}
      ref={ref}
      {...props}
    />
  );
}

export { MyComponent, myComponentVariants };

Step 3: Export It

// index.ts
export { MyComponent, myComponentVariants } from "./MyComponent";

Step 4: Add Stories

Create apps/storybook/src/stories/MyComponent/myComponent.stories.tsx:

import type { Meta, StoryObj } from "@storybook/react-vite";
import { MyComponent } from "@repo/ui";
import { fn } from "storybook/test";

const meta: Meta<typeof MyComponent> = {
  title: "Components/MyComponent",
  component: MyComponent,
  tags: ["autodocs"],
  argTypes: {
    variant: {
      options: ["primary", "secondary", "accent", "destructive", "outline", "ghost"],
      control: { type: "select" },
    },
    size: {
      options: ["default", "sm", "lg", "icon"],
      control: { type: "select" },
    },
  },
  args: {
    onClick: fn(),
    children: "My Component",
  },
};

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

export const Primary: Story = {
  args: {
    variant: "primary",
  },
};

export const Secondary: Story = {
  args: {
    variant: "secondary",
    children: "Secondary",
  },
};

export const Loading: Story = {
  args: {
    isLoading: true,
    children: "Loading...",
  },
};

Step 5: Add to Package

In packages/ui/src/index.ts:

export { MyComponent } from "./components/MyComponent/MyComponent.js";
export { myComponentVariants } from "./components/MyComponent/myComponentVariants.js";

Use explicit named exports in package entrypoints. Do not use export * in public package APIs.

Standard Variants

VariantUsage
primaryMain action (default)
secondaryAlternative action
accentHighlights
destructiveDelete, danger
outlineSecondary with border
ghostMinimal, no background

Checklist

  • All variants have stories in Storybook (Step 4)
  • Stories are documented with JSDoc comments
  • Works in light AND dark mode
  • Focus states: focus-visible:ring-*
  • PropTypes via ComponentProps<"element"> pattern

Components? Check Storybook for live examples.