Web Component Design by @wshobson · Skillet
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.
Just instructions. No commands, network, or file access.
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 ;
}
Use semantic prop names (isLoading vs loading)
Provide sensible defaults
Support composition via children
Allow style overrides via className or style
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, useMemo for expensive renders
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
~1.8K tokens