Agent skill
scaffold-component
Scaffold a new presentational (dumb) component inside an existing feature module, with optional state variants, using shadcn/ui primitives.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/scaffold-component
SKILL.md
Scaffold Component
Scaffold a new component within an existing feature module.
Usage
/scaffold-component <component-name>
Example: /scaffold-component user-profile-card
Instructions
Follow these steps to scaffold a new component:
Step 1: Parse Component Name
The component name is provided in $ARGUMENTS. If $ARGUMENTS is empty or missing, use AskUserQuestion to prompt:
What is the name of the component? (use kebab-case, e.g., user-profile-card)
Step 2: Validate Component Name
Ensure the component name:
- Uses kebab-case (lowercase letters and hyphens only)
- Does not start or end with a hyphen
- Is not empty
If invalid, inform the user and ask for a valid name.
Step 3: Select Target Feature
List all existing features by scanning lib/features/ directory.
If no features exist, inform the user they need to create a feature first using /scaffold-feature.
If only one feature exists, confirm with the user that they want to add the component to that feature.
If multiple features exist, use AskUserQuestion to ask which feature to add the component to:
Question: "Which feature should this component belong to?" Header: "Feature" Options: List existing feature names (up to 4). If more than 4 features exist, show the 4 most recently modified and include guidance to specify "Other" for unlisted features.
Step 4: Ask Which State Files to Include
Use AskUserQuestion with multi-select to ask:
Question: "Which state files should be included?" Header: "States" Options:
- Loading - "Skeleton/loading state shown while data is being fetched"
- Empty - "Empty state shown when there is no data to display"
- Errored - "Error state shown when data fetching fails"
- View - "Separate view component for presentation logic"
Step 5: Ensure Required shadcn Components Exist
IMPORTANT: All feature components MUST be built using shadcn/ui primitives.
Required shadcn components by state file:
| State File | Required shadcn Components |
|---|---|
| Main | card (Card, CardHeader, CardContent) |
| Loading | skeleton |
| Empty | card |
| Errored | card, button |
| View | card |
Check and install missing components:
- Check which shadcn components exist in
components/ui/ - For each missing required component, install it using the shadcn MCP server or CLI:
bash
npx shadcn@latest add <component-name> --yes - Common components to check:
card,skeleton,button,alert
Example installation check:
# Check if card exists
ls components/ui/card.tsx
# If not found, add it
npx shadcn@latest add card --yes
Step 6: Generate Component Files
Create the component directory at lib/features/{feature-name}/components/{component-name}/.
Always create these files:
{component-name}.tsx:
import { type FC } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export interface {ComponentName}Props {
// Add your props here
}
export const {ComponentName}: FC<{ComponentName}Props> = (props: {ComponentName}Props) => {
const {} = props;
return (
<Card>
<CardHeader>
<CardTitle>{ComponentName}</CardTitle>
<CardDescription>Component description</CardDescription>
</CardHeader>
<CardContent>
{/* {ComponentName} content */}
</CardContent>
</Card>
);
};
index.ts:
export { {ComponentName} } from "./{component-name}";
export type { {ComponentName}Props } from "./{component-name}";
Conditionally create state files:
{component-name}-loading.tsx (if Loading selected):
import { type FC } from "react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
export const {ComponentName}Loading: FC = () => {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-[200px]" />
<Skeleton className="h-4 w-[300px]" />
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-[80%]" />
</CardContent>
</Card>
);
};
{component-name}-empty.tsx (if Empty selected):
import { type FC } from "react";
import { Card, CardContent } from "@/components/ui/card";
export interface {ComponentName}EmptyProps {
message?: string;
}
export const {ComponentName}Empty: FC<{ComponentName}EmptyProps> = (props: {ComponentName}EmptyProps) => {
const { message = "No data available" } = props;
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<p className="text-muted-foreground text-center">{message}</p>
</CardContent>
</Card>
);
};
{component-name}-errored.tsx (if Errored selected):
import { type FC } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export interface {ComponentName}ErroredProps {
error?: Error | null;
onRetry?: () => void;
}
export const {ComponentName}Errored: FC<{ComponentName}ErroredProps> = (props: {ComponentName}ErroredProps) => {
const { error, onRetry } = props;
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 gap-4">
<p className="text-destructive text-center">
Something went wrong{error?.message ? `: ${error.message}` : ""}
</p>
{onRetry && (
<Button onClick={onRetry} variant="outline">
Try again
</Button>
)}
</CardContent>
</Card>
);
};
{component-name}-view.tsx (if View selected):
"use client";
import { type FC } from "react";
import { useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
// Presentational component - receives all data and callbacks via props
// No data-fetching hooks allowed here
export interface {ComponentName}ViewProps {
// Data props (passed from main component)
items: unknown[];
// Callback props (passed from main component)
onSelect?: (item: unknown) => void;
}
export const {ComponentName}View: FC<{ComponentName}ViewProps> = (props: {ComponentName}ViewProps) => {
const { items, onSelect } = props;
// UI-only local state is allowed (hover, focus, dropdown visibility)
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
return (
<Card>
<CardHeader>
<CardTitle>{ComponentName}</CardTitle>
<CardDescription>Component description</CardDescription>
</CardHeader>
<CardContent>
{/* Presentation markup - render data from props */}
{items.map((item, index) => (
<div
key={index}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
onClick={() => onSelect?.(item)}
>
{/* Render item */}
</div>
))}
</CardContent>
</Card>
);
};
Update index.ts exports
Add exports for all created state files to index.ts:
export { {ComponentName} } from "./{component-name}";
export type { {ComponentName}Props } from "./{component-name}";
// Add these based on selected states:
export { {ComponentName}Loading } from "./{component-name}-loading";
export { {ComponentName}Empty } from "./{component-name}-empty";
export type { {ComponentName}EmptyProps } from "./{component-name}-empty";
export { {ComponentName}Errored } from "./{component-name}-errored";
export type { {ComponentName}ErroredProps } from "./{component-name}-errored";
export { {ComponentName}View } from "./{component-name}-view";
export type { {ComponentName}ViewProps } from "./{component-name}-view";
Step 7: Output Summary
After creating all files, output a summary:
Created component: {component-name} in {feature-name}
lib/features/{feature-name}/components/{component-name}/
├── {component-name}.tsx
├── {component-name}-loading.tsx
├── {component-name}-empty.tsx
├── {component-name}-errored.tsx
├── {component-name}-view.tsx
└── index.ts
Next steps:
1. Define your component props in {component-name}.tsx
2. Implement the component UI using shadcn components
3. If this component needs data fetching, create a companion hook at:
lib/features/{feature-name}/hooks/use-{component-name}.ts
4. Remember: View components are presentational only - pass all data via props
Adjust the tree output based on which files were actually created.
Naming Conventions
- component-name: kebab-case (e.g.,
user-profile-card) - ComponentName: PascalCase (e.g.,
UserProfileCard)
Convert kebab-case to PascalCase by:
- Splitting on hyphens
- Capitalizing the first letter of each word
- Joining without separators
Example: user-profile-card → UserProfileCard
Dumb Component Pattern
All scaffolded components must follow the "dumb component" (presentational) pattern.
Core Principles
- Components are presentational only - They render UI based on props
- All data comes via props - Data, callbacks, and state are passed down
- No data-fetching hooks in components - No
useSWR,useQuery, or custom data hooks inside view components - Business logic lives in hooks - Create a companion hook if the component needs data fetching
Allowed Hooks in Components
View components may only use hooks for UI-only state:
useStatefor local visual state (dropdown open/closed, hover, focus)useReffor DOM referencesuseCallback/useMemofor UI performance optimization
When to Create a Companion Hook
If the component needs business logic (data fetching, state management, API calls), create a companion hook in the feature's hooks/ directory:
lib/features/{feature-name}/
├── components/{component-name}/
│ ├── {component-name}.tsx # Calls the hook, passes data to view
│ └── {component-name}-view.tsx # Presentational only
└── hooks/
└── use-{component-name}.ts # Business logic
Example Pattern
// Main component calls hook and passes data to view
export const FeatureCard: FC<FeatureCardProps> = (props: FeatureCardProps) => {
const { itemId } = props;
const { items, onSelect } = useFeatureCard({ itemId });
return <FeatureCardView items={items} onSelect={onSelect} />;
};
// View is presentational only - receives everything via props
export const FeatureCardView: FC<FeatureCardViewProps> = (props: FeatureCardViewProps) => {
const { items, onSelect } = props;
// UI-only state is allowed
const [hoveredId, setHoveredId] = useState<string | null>(null);
return (/* render items */);
};
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?