Agents web-component-design
Master React, Vue, and Svelte component patterns including CSS-in-JS, composition strategies, and reusable component architecture. Use when building UI component libraries, designing component APIs, or implementing frontend design systems.
install
source · Clone the upstream repo
git clone https://github.com/wshobson/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/wshobson/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/ui-design/skills/web-component-design" ~/.claude/skills/wshobson-agents-web-component-design && rm -rf "$T"
manifest:
plugins/ui-design/skills/web-component-design/SKILL.mdsource content
Web Component Design
Build reusable, maintainable UI components using modern frameworks with clean composition patterns and styling approaches.
When to Use This Skill
- Designing reusable component libraries or design systems
- Implementing complex component composition patterns
- Choosing and applying CSS-in-JS solutions
- Building accessible, responsive UI components
- Creating consistent component APIs across a codebase
- Refactoring legacy components into modern patterns
- Implementing compound components or render props
Core Concepts
1. Component Composition Patterns
Compound Components: Related components that work together
// Usage <Select value={value} onChange={setValue}> <Select.Trigger>Choose option</Select.Trigger> <Select.Options> <Select.Option value="a">Option A</Select.Option> <Select.Option value="b">Option B</Select.Option> </Select.Options> </Select>
Render Props: Delegate rendering to parent
<DataFetcher url="/api/users"> {({ data, loading, error }) => loading ? <Spinner /> : <UserList users={data} /> } </DataFetcher>
Slots (Vue/Svelte): Named content injection points
<template> <Card> <template #header>Title</template> <template #content>Body text</template> <template #footer><Button>Action</Button></template> </Card> </template>
2. CSS-in-JS Approaches
| Solution | Approach | Best For |
|---|---|---|
| Tailwind CSS | Utility classes | Rapid prototyping, design systems |
| CSS Modules | Scoped CSS files | Existing CSS, gradual adoption |
| styled-components | Template literals | React, dynamic styling |
| Emotion | Object/template styles | Flexible, SSR-friendly |
| Vanilla Extract | Zero-runtime | Performance-critical apps |
3. Component API Design
interface ButtonProps { variant?: "primary" | "secondary" | "ghost"; size?: "sm" | "md" | "lg"; isLoading?: boolean; isDisabled?: boolean; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; children: React.ReactNode; onClick?: () => void; }
Principles:
- Use semantic prop names (
vsisLoading
)loading - Provide sensible defaults
- Support composition via
children - Allow style overrides via
orclassNamestyle
Quick Start: React Component with Tailwind
import { forwardRef, type ComponentPropsWithoutRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { primary: "bg-blue-600 text-white hover:bg-blue-700", secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200", ghost: "hover:bg-gray-100 hover:text-gray-900", }, size: { sm: "h-8 px-3 text-sm", md: "h-10 px-4 text-sm", lg: "h-12 px-6 text-base", }, }, defaultVariants: { variant: "primary", size: "md", }, }, ); interface ButtonProps extends ComponentPropsWithoutRef<"button">, VariantProps<typeof buttonVariants> { isLoading?: boolean; } export const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, isLoading, children, ...props }, ref) => ( <button ref={ref} className={cn(buttonVariants({ variant, size }), className)} disabled={isLoading || props.disabled} {...props} > {isLoading && <Spinner className="mr-2 h-4 w-4" />} {children} </button> ), ); Button.displayName = "Button";
Framework Patterns
React: Compound Components
import { createContext, useContext, useState, type ReactNode } from "react"; interface AccordionContextValue { openItems: Set<string>; toggle: (id: string) => void; } const AccordionContext = createContext<AccordionContextValue | null>(null); function useAccordion() { const context = useContext(AccordionContext); if (!context) throw new Error("Must be used within Accordion"); return context; } export function Accordion({ children }: { children: ReactNode }) { const [openItems, setOpenItems] = useState<Set<string>>(new Set()); const toggle = (id: string) => { setOpenItems((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }; return ( <AccordionContext.Provider value={{ openItems, toggle }}> <div className="divide-y">{children}</div> </AccordionContext.Provider> ); } Accordion.Item = function AccordionItem({ id, title, children, }: { id: string; title: string; children: ReactNode; }) { const { openItems, toggle } = useAccordion(); const isOpen = openItems.has(id); return ( <div> <button onClick={() => toggle(id)} className="w-full text-left py-3"> {title} </button> {isOpen && <div className="pb-3">{children}</div>} </div> ); };
Vue 3: Composables
<script setup lang="ts"> import { ref, computed, provide, inject, type InjectionKey } from "vue"; interface TabsContext { activeTab: Ref<string>; setActive: (id: string) => void; } const TabsKey: InjectionKey<TabsContext> = Symbol("tabs"); // Parent component const activeTab = ref("tab-1"); provide(TabsKey, { activeTab, setActive: (id: string) => { activeTab.value = id; }, }); // Child component usage const tabs = inject(TabsKey); const isActive = computed(() => tabs?.activeTab.value === props.id); </script>
Svelte 5: Runes
<script lang="ts"> interface Props { variant?: 'primary' | 'secondary'; size?: 'sm' | 'md' | 'lg'; onclick?: () => void; children: import('svelte').Snippet; } let { variant = 'primary', size = 'md', onclick, children }: Props = $props(); const classes = $derived( `btn btn-${variant} btn-${size}` ); </script> <button class={classes} {onclick}> {@render children()} </button>
Best Practices
- Single Responsibility: Each component does one thing well
- Prop Drilling Prevention: Use context for deeply nested data
- Accessible by Default: Include ARIA attributes, keyboard support
- Controlled vs Uncontrolled: Support both patterns when appropriate
- Forward Refs: Allow parent access to DOM nodes
- Memoization: Use
,React.memo
for expensive rendersuseMemo - Error Boundaries: Wrap components that may fail
Common Issues
- Prop Explosion: Too many props - consider composition instead
- Style Conflicts: Use scoped styles or CSS Modules
- Re-render Cascades: Profile with React DevTools, memo appropriately
- Accessibility Gaps: Test with screen readers and keyboard navigation
- Bundle Size: Tree-shake unused component variants