Component Scaffold Generator
Generate production-ready component skeletons with types, variants, tests, and documentation.
Core Workflow
-
Gather requirements: Component name, framework (React/Vue), props needed
-
Choose pattern: Determine if functional, compound, or polymorphic component
-
Generate component: Create main component file with TypeScript types
-
Add variants: Include common variants (size, color, state)
-
Setup styling: Add styling approach (Tailwind, CSS Modules, styled-components)
-
Create tests: Generate test file with basic coverage
-
Add story: Create Storybook story with examples
-
Document usage: Include JSDoc and usage examples
Component Patterns
Basic Functional Component (React)
// Button.tsx import { forwardRef } from "react"; import { cn } from "@/lib/utils";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: "primary" | "secondary" | "ghost" | "destructive"; size?: "sm" | "md" | "lg"; isLoading?: boolean; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; }
/**
- Button component with multiple variants and sizes
- @example
-
- <Button variant="primary" size="md">
- Click me
- </Button>
-
*/ export const Button = forwardRef<HTMLButtonElement, ButtonProps>( ( { variant = "primary", size = "md", isLoading = false, leftIcon, rightIcon, className, children, disabled, ...props }, ref ) => { return ( <button ref={ref} className={cn( "inline-flex items-center justify-center rounded-md font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2", "disabled:pointer-events-none disabled:opacity-50", { // Variants "bg-blue-600 text-white hover:bg-blue-700": variant === "primary", "bg-gray-200 text-gray-900 hover:bg-gray-300": variant === "secondary", "hover:bg-gray-100": variant === "ghost", "bg-red-600 text-white hover:bg-red-700": variant === "destructive", // Sizes "h-8 px-3 text-sm": size === "sm", "h-10 px-4 text-base": size === "md", "h-12 px-6 text-lg": size === "lg", }, className )} disabled={disabled || isLoading} {...props} > {isLoading && ( <svg className="mr-2 h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" > <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> </svg> )} {leftIcon && <span className="mr-2">{leftIcon}</span>} {children} {rightIcon && <span className="ml-2">{rightIcon}</span>} </button> ); } );
Button.displayName = "Button";
Compound Component Pattern
// Card.tsx import { createContext, useContext } from "react";
interface CardContextValue { variant: "default" | "outlined" | "elevated"; }
const CardContext = createContext<CardContextValue | undefined>(undefined);
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> { variant?: "default" | "outlined" | "elevated"; }
export const Card = ({ variant = "default", className, children, ...props }: CardProps) => { return ( <CardContext.Provider value={{ variant }}> <div className={cn("rounded-lg", className)} {...props}> {children} </div> </CardContext.Provider> ); };
export const CardHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn("p-6", className)} {...props} /> );
export const CardTitle = ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => ( <h3 className={cn("text-2xl font-semibold", className)} {...props} /> );
export const CardContent = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn("p-6 pt-0", className)} {...props} /> );
export const CardFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn("flex items-center p-6 pt-0", className)} {...props} /> );
Test File Template
// Button.test.tsx import { render, screen, fireEvent } from "@testing-library/react"; import { Button } from "./Button";
describe("Button", () => { it("renders children correctly", () => { render(<Button>Click me</Button>); expect( screen.getByRole("button", { name: /click me/i }) ).toBeInTheDocument(); });
it("applies variant classes correctly", () => { const { rerender } = render(<Button variant="primary">Test</Button>); expect(screen.getByRole("button")).toHaveClass("bg-blue-600");
rerender(<Button variant="secondary">Test</Button>);
expect(screen.getByRole("button")).toHaveClass("bg-gray-200");
});
it("handles click events", () => { const handleClick = jest.fn(); render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("shows loading state", () => { render(<Button isLoading>Loading</Button>); expect(screen.getByRole("button")).toBeDisabled(); expect(screen.getByRole("button")).toContainHTML("svg"); });
it("renders with icons", () => { render( <Button leftIcon={<span data-testid="left">←</span>}>With Icon</Button> ); expect(screen.getByTestId("left")).toBeInTheDocument(); });
it("forwards ref correctly", () => { const ref = React.createRef<HTMLButtonElement>(); render(<Button ref={ref}>Test</Button>); expect(ref.current).toBeInstanceOf(HTMLButtonElement); }); });
Storybook Story Template
// Button.stories.tsx import type { Meta, StoryObj } from "@storybook/react"; import { Button } from "./Button";
const meta: Meta<typeof Button> = { title: "Components/Button", component: Button, tags: ["autodocs"], argTypes: { variant: { control: "select", options: ["primary", "secondary", "ghost", "destructive"], }, size: { control: "select", options: ["sm", "md", "lg"], }, isLoading: { control: "boolean", }, }, };
export default meta; type Story = StoryObj<typeof Button>;
export const Primary: Story = { args: { variant: "primary", children: "Button", }, };
export const Secondary: Story = { args: { variant: "secondary", children: "Button", }, };
export const WithIcons: Story = { args: { children: "With Icons", leftIcon: "←", rightIcon: "→", }, };
export const Loading: Story = { args: { children: "Loading", isLoading: true, }, };
export const Sizes: Story = { render: () => ( <div className="flex gap-4 items-center"> <Button size="sm">Small</Button> <Button size="md">Medium</Button> <Button size="lg">Large</Button> </div> ), };
export const Variants: Story = { render: () => ( <div className="flex gap-4"> <Button variant="primary">Primary</Button> <Button variant="secondary">Secondary</Button> <Button variant="ghost">Ghost</Button> <Button variant="destructive">Destructive</Button> </div> ), };
Vue Component Pattern
<!-- Button.vue --> <script setup lang="ts"> import { computed } from "vue";
interface Props { variant?: "primary" | "secondary" | "ghost" | "destructive"; size?: "sm" | "md" | "lg"; isLoading?: boolean; disabled?: boolean; }
const props = withDefaults(defineProps<Props>(), { variant: "primary", size: "md", isLoading: false, disabled: false, });
const emit = defineEmits<{ click: [event: MouseEvent]; }>();
const buttonClasses = computed(() => { return [ "inline-flex items-center justify-center rounded-md font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2", "disabled:pointer-events-none disabled:opacity-50", // Variants { "bg-blue-600 text-white hover:bg-blue-700": props.variant === "primary", "bg-gray-200 text-gray-900 hover:bg-gray-300": props.variant === "secondary", "hover:bg-gray-100": props.variant === "ghost", "bg-red-600 text-white hover:bg-red-700": props.variant === "destructive", }, // Sizes { "h-8 px-3 text-sm": props.size === "sm", "h-10 px-4 text-base": props.size === "md", "h-12 px-6 text-lg": props.size === "lg", }, ]; });
const handleClick = (event: MouseEvent) => { if (!props.disabled && !props.isLoading) { emit("click", event); } }; </script>
<template> <button :class="buttonClasses" :disabled="disabled || isLoading" @click="handleClick"
<svg
v-if="isLoading"
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<slot />
</button> </template>
Index File (Barrel Export)
// index.ts export { Button } from "./Button"; export type { ButtonProps } from "./Button";
Usage Documentation Template
Button Component
A flexible button component with multiple variants, sizes, and states.
Installation
npm install @/components/ui/button
Usage
import { Button } from "@/components/ui/button";
function App() { return ( <Button variant="primary" size="md" onClick={() => console.log("Clicked!")}> Click me </Button> ); }
Props
Prop Type Default Description
variant 'primary' | 'secondary' | 'ghost' | 'destructive' 'primary' Visual style variant
size 'sm' | 'md' | 'lg' 'md' Button size
isLoading boolean false Shows loading spinner
leftIcon ReactNode
Icon before text
rightIcon ReactNode
Icon after text
disabled boolean false Disables the button
Examples
With Icons
<Button leftIcon={<ChevronLeft />}>Back</Button>
Loading State
<Button isLoading>Saving...</Button>
Destructive Action
<Button variant="destructive" onClick={handleDelete}> Delete Account </Button>
Accessibility
-
Keyboard navigable
-
ARIA attributes included
-
Focus visible styling
-
Disabled state properly handled
Component Structure Template
ComponentName/ ├── ComponentName.tsx # Main component ├── ComponentName.test.tsx # Tests ├── ComponentName.stories.tsx # Storybook ├── types.ts # TypeScript types (if complex) ├── styles.module.css # CSS Modules (if used) └── index.ts # Barrel export
Common Component Types
UI Primitives
- Button, Input, Select, Checkbox, Radio
- Typography (Text, Heading, Label)
- Icons, Avatar, Badge
Layout Components
- Container, Stack, Grid, Flex
- Card, Panel, Section
- Divider, Spacer
Data Display
- Table, List, DataGrid
- Tooltip, Popover, Dropdown
- Tabs, Accordion, Collapse
Feedback
- Alert, Toast, Modal
- Progress, Spinner, Skeleton
- EmptyState, ErrorBoundary
Forms
- FormField, FormGroup, FormLabel
- FileUpload, DatePicker, RangePicker
- SearchInput, TagInput
Best Practices
- Forward refs: Use
forwardReffor DOM access - Type props properly: Extend native HTML element props
- Use composition: Compound components when appropriate
- Accessibility first: ARIA attributes, keyboard nav
- Variants system: Size, color, state variants
- Loading states: Handle async operations
- Error states: Validate and show errors
- Test coverage: At least 80% coverage
- Document usage: JSDoc and README examples
- Export types: Make TypeScript types available
Styling Approaches
Tailwind CSS (Recommended)
- Use
cn()utility for conditional classes - Define variants with object syntax
- Responsive utilities built-in
CSS Modules
- Scoped styles by default
- Type-safe with TypeScript
- Better for complex animations
Styled Components
- CSS-in-JS with props
- Dynamic theming support
- Runtime styling
Output Checklist
Every generated component should include:
- Main component file with TypeScript types
- Prop interface with JSDoc comments
- Multiple variants (size, color, state)
- Accessibility attributes
- Test file with key scenarios
- Storybook story with examples
- Barrel export (index.ts)
- Usage documentation
- Error handling
- Loading states (if applicable)