How this skill is triggered — by the user, by Claude, or both
Slash command
/design-engineer:component-craftThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Build React + TypeScript components that are small, composable, accessible, and precisely typed. Default to the simplest component that solves the problem; add flexibility only when a second real use case demands it.
Build React + TypeScript components that are small, composable, accessible, and precisely typed. Default to the simplest component that solves the problem; add flexibility only when a second real use case demands it.
children) over configuration (a wall of boolean props).asChild / polymorphism only when consumers genuinely need to swap the rendered element. It is not free — skip it by default.type ButtonProps = ComponentPropsWithoutRef<"button"> & {
variant?: "solid" | "outline" | "ghost";
size?: "sm" | "md";
loading?: boolean;
};
When a component starts growing booleans like showHeader, showFooter, withIcon, that is the signal to expose compound subcomponents instead:
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>…</Card.Body>
</Card>
Compound components let consumers compose what they need without you predicting every layout. Use a boolean prop only for a genuine binary toggle.
Keep variant logic in one typed map keyed by the prop union. cva works; a small typed record is enough and dependency-free:
const styles: Record<NonNullable<ButtonProps["variant"]>, string> = {
solid: "bg-blue-600 text-white hover:bg-blue-700",
outline: "border border-blue-600 text-blue-600 hover:bg-blue-50",
ghost: "text-blue-600 hover:bg-blue-50",
};
The Record over the union forces every variant to be handled — adding a variant to the union and forgetting its style becomes a compile error.
A component is not done until all states are styled and reachable:
default · hover · focus-visible · active · disabled · loading · error
:focus-visible (not :focus) so keyboard users get a ring without mouse-click rings.disabled must block interaction and convey it (disabled attribute / aria-disabled), not just dim opacity.loading should set aria-busy and prevent duplicate submits.<button> before a <div role="button">. Native semantics give you keyboard, focus, and roles for free.role / aria-* only where semantics fall short (custom widgets: tabs, combobox, dialog).aria-label).ComponentPropsWithoutRef<"button"> so consumers get the full native surface.ComponentPropsWithoutRef together with forwardRef for ref-forwarding components.Select<T> whose value/onChange track the option type). Do not add generics gratuitously.useEffect to derive state from props — compute during render. Use it only for genuine external synchronization (subscriptions, focus management, non-React DOM).import { ComponentPropsWithoutRef, forwardRef } from "react";
type ButtonProps = ComponentPropsWithoutRef<"button"> & {
variant?: "solid" | "outline" | "ghost";
size?: "sm" | "md";
loading?: boolean;
};
const variantStyles: Record<NonNullable<ButtonProps["variant"]>, string> = {
solid: "bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800",
outline: "border border-blue-600 text-blue-600 hover:bg-blue-50",
ghost: "text-blue-600 hover:bg-blue-50",
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "solid", size = "md", loading = false, disabled, className, children, ...rest }, ref) => (
<button
ref={ref}
disabled={disabled || loading}
aria-busy={loading}
className={[
"inline-flex items-center justify-center rounded font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
"disabled:cursor-not-allowed disabled:opacity-50",
size === "sm" ? "h-8 px-3 text-sm" : "h-10 px-4",
variantStyles[variant],
className,
]
.filter(Boolean)
.join(" ")}
{...rest}
>
{children}
</button>
),
);
Button.displayName = "Button";
This shows the whole pattern: native props extended, discriminated variant union, exhaustive variant map, ref forwarding, rest-prop spreading, focus-visible ring, and merged disabled + loading state with aria-busy.
children) or context.id, aria-*, type). Spread ...rest to the root instead.npx claudepluginhub shoto290/shoto --plugin design-engineerProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.