Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/wshobson-web-component-design/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 81% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 76% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 87% | 0% |
Build reusable, maintainable UI components using modern frameworks with clean composition patterns and styling approaches.
Compound Components: Related components that work together
tsx// 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
tsx<DataFetcher url="/api/users"> {({ data, loading, error }) => loading ? <Spinner /> : <UserList users={data} /> } </DataFetcher>
Slots (Vue/Svelte): Named content injection points
vue<template> <Card> <template #header>Title</template> <template #content>Body text</template> <template #footer><Button>Action</Button></template> </Card> </template>
| 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 |
tsxinterface 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:
isLoading vs loading)childrenclassName or styletsximport { 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";
tsximport { 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<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<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>
React.memo, useMemo for expensive renders| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 13,826 | 14,556 | +5% | 1 | 1 | 0% | 2,816 | 5,097 | +81% | 0 | 0 | — |
case-02 | pass→pass | 12,181 | 13,558 | +11% | 1 | 1 | 0% | 2,786 | 4,897 | +76% | 0 | 0 | — |
case-03 | fail→pass | 11,214 | 9,209 | -18% | 1 | 1 | 0% | 2,253 | 3,873 | +72% | 0 | 0 | — |
case-04 | pass→pass | 7,990 | 5,647 | -29% | 1 | 1 | 0% | 1,648 | 3,079 | +87% | 0 | 0 | — |
case-05 | pass→pass | 7,462 | 7,676 | +3% | 1 | 1 | 0% | 1,484 | 3,342 | +125% | 0 | 0 | — |
case-06 | pass→pass | 8,397 | 6,583 | -22% | 1 | 1 | 0% | 1,718 | 3,144 | +83% | 0 | 0 | — |
case-07 | pass→pass | 5,711 | 5,573 | -2% | 1 | 1 | 0% | 1,102 | 3,099 | +181% | 0 | 0 | — |
case-08 | pass→pass | 5,947 | 6,174 | +4% | 1 | 1 | 0% | 1,217 | 3,185 | +162% | 0 | 0 | — |
case-09 | pass→pass | 12,905 | 9,313 | -28% | 1 | 1 | 0% | 2,031 | 4,030 | +98% | 0 | 0 | — |
case-10 | pass→pass | 9,395 | 9,867 | +5% | 1 | 1 | 0% | 1,965 | 3,918 | +99% | 0 | 0 | — |
case-11 | pass→pass | 12,144 | 9,895 | -19% | 1 | 1 | 0% | 2,077 | 3,627 | +75% | 0 | 0 | — |
case-12 | pass→pass | 10,069 | 8,252 | -18% | 1 | 1 | 0% | 1,751 | 3,560 | +103% | 0 | 0 | — |
case-13 | pass→pass | 9,934 | 11,267 | +13% | 1 | 1 | 0% | 1,929 | 4,252 | +120% | 0 | 0 | — |
case-14 | pass→pass | 13,940 | 15,035 | +8% | 1 | 1 | 0% | 2,594 | 4,828 | +86% | 0 | 0 | — |
case-15 | fail→pass | 12,643 | 13,938 | +10% | 1 | 1 | 0% | 2,241 | 4,329 | +93% | 0 | 0 | — |
case-16 | pass→pass | 9,650 | 10,088 | +5% | 1 | 1 | 0% | 2,051 | 3,861 | +88% | 0 | 0 | — |
case-17 | pass→pass | 9,413 | 6,331 | -33% | 1 | 1 | 0% | 1,880 | 3,212 | +71% | 0 | 0 | — |
case-18 | pass→pass | 7,798 | 6,853 | -12% | 1 | 1 | 0% | 1,616 | 3,188 | +97% | 0 | 0 | — |
case-19 | pass→pass | 9,471 | 11,850 | +25% | 1 | 1 | 0% | 1,975 | 4,344 | +120% | 0 | 0 | — |
case-20 | pass→pass | 14,735 | 14,474 | -2% | 1 | 1 | 0% | 2,931 | 4,936 | +68% | 0 | 0 | — |
case-21 | pass→pass | 13,568 | 10,647 | -22% | 1 | 1 | 0% | 2,199 | 3,715 | +69% | 0 | 0 | — |
case-22 | pass→pass | 13,466 | 13,818 | +3% | 1 | 1 | 0% | 2,398 | 4,645 | +94% | 0 | 0 | — |
case-23 | pass→pass | 7,275 | 4,972 | -32% | 1 | 1 | 0% | 1,406 | 2,927 | +108% | 0 | 0 | — |
case-24 | pass→pass | 12,256 | 12,578 | +3% | 1 | 1 | 0% | 2,790 | 4,723 | +69% | 0 | 0 | — |
case-25 | pass→pass | 11,225 | 10,701 | -5% | 1 | 1 | 0% | 2,119 | 4,106 | +94% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +8 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.