The React Foundation Store Design System is a layered component architecture designed for composability, maintainability, and scalability.
Core Principles:
- Small & Composable - Components do one thing well
- Layered Architecture - Clear separation between primitives, components, and features
- Consistent Namespace - All components accessible via
RFDS.* - TypeScript-first - Full type safety
- Variants-based - Flexible styling through variant props
Location: src/components/ui/*
Base-level, highly reusable building blocks. No business logic, minimal composition.
Components:
Button/ButtonLink- Buttons with variants (primary, secondary, ghost, glass, link)Pill- Badge/tag componentRating- Star rating displayCollapsible- Expandable/collapsible content wrapperScrollReveal- Scroll-triggered animation wrapperInput- Text input field with semantic themingTextarea- Multi-line text inputSelect- Dropdown select menuLabel- Form label componentCheckbox- Checkbox inputRadio- Radio button inputSwitch- Toggle switch componentSeparator- Visual divider/separatorDialog- Modal dialog componentTooltip- Tooltip component
Usage:
import { RFDS } from "@/components/rfds"
<RFDS.Button variant="primary" size="lg">
Click me
</RFDS.Button>
<RFDS.Pill>New</RFDS.Pill>
<RFDS.Rating value={4.5} count={312} />Characteristics:
- No data fetching
- No business logic
- Highly reusable
- Variant-based styling
- Minimal dependencies
Location: src/components/ui/* (composed components)
Composed from primitives, still reusable but more specific to domain.
Components:
ProductCard- Product preview card (uses Button, Rating, Pill internally)ProductGallery- Image carousel with lightboxTable- Fully-featured data table with search, sorting, and custom renderingStatCard- Unified stat/info/metric card componentFormInput- Form input with label, helper text, and error handlingSearchInput- Search input with icon
Usage:
import { RFDS } from "@/components/rfds"
<RFDS.ProductCard
product={product}
href="/products/fiber-shell"
/>
<RFDS.ProductGallery images={images} />Characteristics:
- Composed from multiple primitives
- Domain-specific (products, collections)
- Still reusable across pages
- No data fetching (accepts props)
Location: src/components/layout/*
Page structure components.
Components:
Header- Global navigation header (fixed, frosted glass)Footer- Site footer with links
Usage:
import { RFDS } from "@/components/rfds"
<RFDS.Header />
<main>...</main>
<RFDS.Footer />Characteristics:
- Control page structure
- Appear across multiple pages
- Minimal props (mostly self-contained)
Location: src/features/*
Complex, feature-specific components with business logic.
Examples:
MaintainerProgress- GitHub contribution trackingImpactSection- Foundation impact statsSignInButton- Authentication UI
Usage:
// Import directly, not via RFDS
import { MaintainerProgress } from "@/features/maintainer-progress/maintainer-progress"
<MaintainerProgress />Characteristics:
- Contains business logic
- May include custom hooks
- Feature-specific (not generic)
- Can fetch data
import { RFDS } from "@/components/rfds"
export function MyPage() {
return (
<div>
<RFDS.Button variant="primary">
Primary Action
</RFDS.Button>
<RFDS.Pill>New</RFDS.Pill>
<RFDS.ProductCard product={product} href="/products/item" />
</div>
)
}Benefits:
- Clear component origin
- Autocomplete-friendly
- Easy to discover available components
- Consistent import style
import { Button, Pill } from "@/components/rfds"
export function MyPage() {
return (
<div>
<Button variant="primary">Primary Action</Button>
<Pill>New</Pill>
</div>
)
}Benefits:
- Shorter syntax
- Tree-shaking friendly
- Backwards compatible
import { RFDS } from "@/components/rfds"
<RFDS.Primitives.Button variant="primary" />
<RFDS.Components.ProductCard product={product} />
<RFDS.Layouts.Header />Use case: When you want to be explicit about component layers
Variants: primary, secondary, ghost, glass, link
Sizes: xs, sm, md, lg
<RFDS.Button variant="primary" size="lg">
Primary Button
</RFDS.Button>
<RFDS.ButtonLink href="/collections" variant="secondary" size="md">
Link Button
</RFDS.ButtonLink>Props:
type ButtonProps = {
variant?: "primary" | "secondary" | "ghost" | "glass" | "link";
size?: "xs" | "sm" | "md" | "lg";
className?: string;
children: ReactNode;
// ... standard button props
}Badge/tag component for labels.
<RFDS.Pill>New</RFDS.Pill>
<RFDS.Pill tone="sky">Limited Edition</RFDS.Pill>Props:
type PillProps = {
tone?: "sky" | "emerald" | "rose";
className?: string;
children: ReactNode;
}Star rating display with React logo as stars.
<RFDS.Rating value={4.8} count={312} size="md" />Props:
type RatingProps = {
value: number; // 0-5
count?: number; // Review count
size?: "sm" | "md" | "lg";
className?: string;
}Expandable/collapsible content with smooth animation.
<RFDS.Collapsible trigger={<p>Click to expand</p>}>
<p>Hidden content here</p>
</RFDS.Collapsible>Props:
type CollapsibleProps = {
trigger: ReactNode;
children: ReactNode;
defaultOpen?: boolean;
className?: string;
}Scroll-triggered animations (Apple-style).
<RFDS.ScrollReveal animation="fade-up" delay={100}>
<div>Content reveals on scroll</div>
</RFDS.ScrollReveal>Animations: fade, fade-up, fade-down, scale, slide-left, slide-right
Props:
type ScrollRevealProps = {
children: ReactNode;
animation?: "fade" | "fade-up" | "fade-down" | "scale" | "slide-left" | "slide-right";
delay?: number; // milliseconds
threshold?: number; // 0-1
triggerOnce?: boolean;
}Text input field with semantic theming and variants.
<RFDS.Input
type="text"
placeholder="Enter text..."
variant="default"
/>
<RFDS.Input variant="error" />
<RFDS.Input variant="success" />Props:
type InputProps = {
variant?: "default" | "error" | "success";
type?: string;
className?: string;
// ... standard input props
}Multi-line text input with semantic theming.
<RFDS.Textarea
rows={4}
placeholder="Enter description..."
/>Props:
type TextareaProps = {
rows?: number;
className?: string;
// ... standard textarea props
}Dropdown select menu with semantic theming.
<RFDS.Select>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</RFDS.Select>Form label component with optional required indicator.
<RFDS.Label htmlFor="input-id" required>
Field Label
</RFDS.Label>Props:
type LabelProps = {
htmlFor?: string;
required?: boolean;
children: ReactNode;
}Checkbox input with semantic styling.
<RFDS.Checkbox
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>Radio button input with semantic styling.
<RFDS.Radio
name="group"
value="option1"
checked={selected === "option1"}
/>Toggle switch component.
<RFDS.Switch
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
/>Visual divider/separator component.
<RFDS.Separator />
<RFDS.Separator orientation="vertical" />Props:
type SeparatorProps = {
orientation?: "horizontal" | "vertical";
className?: string;
}Modal dialog component for overlays and modals.
<RFDS.Dialog open={isOpen} onOpenChange={setIsOpen}>
<RFDS.Dialog.Trigger>Open Dialog</RFDS.Dialog.Trigger>
<RFDS.Dialog.Content>
<RFDS.Dialog.Header>
<RFDS.Dialog.Title>Dialog Title</RFDS.Dialog.Title>
</RFDS.Dialog.Header>
<RFDS.Dialog.Description>
Dialog description
</RFDS.Dialog.Description>
</RFDS.Dialog.Content>
</RFDS.Dialog>Tooltip component for hover information.
<RFDS.Tooltip content="Helpful information">
<button>Hover me</button>
</RFDS.Tooltip>Props:
type TooltipProps = {
content: ReactNode;
children: ReactNode;
side?: "top" | "right" | "bottom" | "left";
}Product preview card with image, name, price, rating.
<RFDS.ProductCard
product={product}
href="/products/fiber-shell"
ctaLabel="View product"
/>Props:
type ProductCardProps = {
product: Product;
href: string;
ctaLabel?: string;
}Image carousel/lightbox for product detail pages.
<RFDS.ProductGallery images={product.images} />Props:
type ProductGalleryProps = {
images: ProductImage[];
}Fully-featured data table with search, sorting, and custom rendering.
const columns: TableColumn<User>[] = [
{
key: 'name',
label: 'Name',
sortable: true,
render: (_value, user) => <div>{user.name}</div>,
accessor: (user) => user.name.toLowerCase(),
},
{
key: 'email',
label: 'Email',
sortable: true,
accessor: (user) => user.email,
},
];
<RFDS.Table
data={users}
columns={columns}
searchable
searchPlaceholder="Search users..."
defaultSortKey="name"
defaultSortDirection="asc"
getRowKey={(user) => user.id}
/>Features:
- Generic type support for any data structure
- Search/filter functionality
- Column sorting (ascending/descending)
- Computed columns (derived from data)
- Custom cell rendering
- Semantic theming
Props:
type TableProps<T> = {
data: T[];
columns: TableColumn<T>[];
searchable?: boolean;
searchPlaceholder?: string;
searchFn?: (item: T, query: string) => boolean;
defaultSortKey?: string;
defaultSortDirection?: 'asc' | 'desc' | null;
showEmptyState?: boolean;
emptyStateMessage?: string;
getRowKey: (item: T) => string;
}
type TableColumn<T> = {
key: string;
label: string;
sortable?: boolean;
align?: 'left' | 'center' | 'right';
render?: (value: unknown, item: T) => ReactNode;
accessor?: (item: T) => string | number;
computed?: (item: T) => string | number;
}Unified stat/info/metric card component. Replaces duplicate StatCard, InfoCard, and MetricCard implementations.
<RFDS.StatCard
label="Total Users"
value="1,234"
detail="+12% from last month"
icon="👥"
trend="up"
variant="outlined"
color="primary"
/>
<RFDS.StatCard
label="Revenue"
value="$50,000"
highlight
color="success"
/>Props:
type StatCardProps = {
value: string | number;
label: string;
detail?: string;
icon?: string | ReactNode;
trend?: 'up' | 'down' | 'neutral';
highlight?: boolean;
variant?: 'default' | 'outlined' | 'elevated';
color?: 'primary' | 'success' | 'destructive' | 'warning';
className?: string;
}Composition component for form inputs with label, helper text, and error handling.
<RFDS.FormInput
label="Email Address"
type="email"
required
helperText="We'll never share your email"
errorText={errors.email}
variant={errors.email ? 'error' : 'default'}
/>Props:
type FormInputProps = {
label?: string;
helperText?: string;
errorText?: string;
required?: boolean;
id?: string;
variant?: 'default' | 'error' | 'success';
// ... Input props
}Search input with built-in search icon.
<RFDS.SearchInput
placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>Props:
type SearchInputProps = {
placeholder?: string;
// ... Input props
}Global navigation header (fixed position, frosted glass).
<RFDS.Header />Features:
- Fixed position with blur effect
- React Foundation logo (links to home)
- Navigation links (Collections, Limited Drops, Impact)
- Sign in button
- Cart icon with count badge
Site footer with copyright and links.
<RFDS.Footer />Features:
- Copyright notice (auto-updates year)
- Legal links (Privacy, Terms, Accessibility)
All components use consistent design tokens:
Colors:
- Primary: Sky/Indigo/Purple gradients
- Secondary: Slate with glass effects
- Accent: Emerald (success), Rose (error), Amber (warning)
Typography:
- Font: Geist Sans (primary), Geist Mono (code)
- Scale: text-xs → text-5xl
- Tracking: 0.25em (labels), 0.3em (caps), 0.6em (hero)
Spacing:
- Scale: 0.5rem → 20rem (Tailwind default)
- Gap: 4-20 (between sections)
Borders:
- Radius: rounded-xl (cards), rounded-full (pills/buttons)
- Color: white/10 (default), white/20 (hover)
Effects:
- Blur: backdrop-blur-xl (glass)
- Shadow: shadow-lg shadow-black/20
- Transitions: duration-300 ease-in-out
Primary:
<RFDS.Button variant="primary">
// Gradient background, bold, high-contrast
</RFDS.Button>Secondary:
<RFDS.Button variant="secondary">
// Border, transparent background, glass effect
</RFDS.Button>Ghost:
<RFDS.Button variant="ghost">
// Minimal, text-focused, subtle hover
</RFDS.Button>Glass:
<RFDS.Button variant="glass">
// Frosted glass effect, blur
</RFDS.Button>Link:
<RFDS.Button variant="link">
// Text-only, underline on hover
</RFDS.Button>✅ Good:
import { RFDS } from "@/components/rfds"
<RFDS.Button variant="primary">Click</RFDS.Button>❌ Avoid:
import { Button } from "@/components/ui/button"
<Button variant="primary">Click</Button>When creating new components:
- Single responsibility
- Accept props, don't fetch data
- Compose from existing primitives
- Add to appropriate layer
✅ Good:
<RFDS.Button variant="ghost" size="sm">
Small Action
</RFDS.Button>❌ Avoid:
<RFDS.Button className="px-2 py-1 text-xs bg-transparent">
Small Action
</RFDS.Button>Build new components from existing ones:
// Good - composes from primitives
export function DropCard({ drop }) {
return (
<div>
<RFDS.Pill>{drop.season}</RFDS.Pill>
<h3>{drop.title}</h3>
<RFDS.ButtonLink href={`/collections/${drop.handle}`} variant="ghost">
View drop
</RFDS.ButtonLink>
</div>
)
}Is it a primitive?
- Single purpose
- No composition
- Highly reusable
→ Add to
src/components/ui/
Is it a component?
- Composed from primitives
- Domain-specific
- Still reusable
→ Add to
src/components/ui/or create new category
Is it a feature?
- Complex business logic
- Feature-specific
- Not generic
→ Add to
src/features/
Example: Adding a new Badge primitive
Create file: src/components/ui/badge.tsx
interface BadgeProps {
variant?: "default" | "success" | "error";
children: ReactNode;
}
export function Badge({ variant = "default", children }: BadgeProps) {
// Implementation
}Update: src/components/rfds/primitives.ts
export { Badge } from "@/components/ui/badge";
import { Badge } from "@/components/ui/badge";
export const Primitives = {
// ... existing
Badge,
};Update: src/components/rfds/index.ts
import { Badge } from "./primitives";
export const RFDS = {
// ... existing primitives
Badge,
};
export { Badge };Add to this file with usage examples and props.
- ✅ Button / ButtonLink
- ✅ Pill
- ✅ Rating
- ✅ Collapsible
- ✅ ScrollReveal
- ✅ Input
- ✅ Textarea
- ✅ Select
- ✅ Label
- ✅ Checkbox
- ✅ Radio
- ✅ Switch
- ✅ Separator
- ✅ Dialog
- ✅ Tooltip
- ⬜ Typography (planned)
- ✅ ProductCard
- ✅ ProductGallery
- ✅ Table
- ✅ StatCard
- ✅ FormInput
- ✅ SearchInput
- ⬜ CollectionCard (planned)
- ⬜ DropCard (planned)
- ✅ Header
- ✅ Footer
- Form primitives (Input, Select, Checkbox, Radio, Switch, Label, Textarea)
- Layout primitives (Separator, Dialog, Tooltip)
- Table component with search and sorting
- Composition components (StatCard, FormInput, SearchInput)
- Typography primitives (Heading, Text)
- Card primitive (generic card component)
- CollectionCard component
- DropCard component
- Dropdown menu primitive
- Tabs component
- Breadcrumbs component
- Theme tokens (CSS variables)
- Dark/light mode toggle
- Accessibility audit
- Storybook integration
- Component unit tests
Before:
import { Button, ButtonLink } from "@/components/ui/button";
import { ProductCard } from "@/components/ui/product-card";
import { Rating } from "@/components/ui/rating";
export function MyComponent() {
return (
<div>
<Button variant="primary">Click</Button>
<ProductCard product={p} href="/products/x" />
<Rating value={4.5} />
</div>
);
}After:
import { RFDS } from "@/components/rfds";
export function MyComponent() {
return (
<div>
<RFDS.Button variant="primary">Click</RFDS.Button>
<RFDS.ProductCard product={p} href="/products/x" />
<RFDS.Rating value={4.5} />
</div>
);
}Steps:
- Replace individual imports with
import { RFDS } from "@/components/rfds" - Prefix all component usage with
RFDS. - Test that everything still works
import { RFDS } from "@/components/rfds";
export default function NotFound() {
return (
<main>
<h1>Page Not Found</h1>
<div>
<RFDS.ButtonLink href="/" size="lg">
Return to storefront
</RFDS.ButtonLink>
<RFDS.ButtonLink href="/collections" size="lg" variant="secondary">
Explore collections
</RFDS.ButtonLink>
<RFDS.ButtonLink href="/#drops" size="lg" variant="ghost">
View drops
</RFDS.ButtonLink>
</div>
</main>
);
}import { RFDS } from "@/components/rfds";
export function ProductList({ products }) {
return (
<div>
<RFDS.Pill>Featured</RFDS.Pill>
<div className="grid gap-6 md:grid-cols-3">
{products.map((product) => (
<RFDS.ProductCard
key={product.slug}
product={product}
href={`/products/${product.slug}`}
/>
))}
</div>
<RFDS.ButtonLink href="/collections" variant="ghost">
View all →
</RFDS.ButtonLink>
</div>
);
}import { RFDS } from "@/components/rfds";
export function CollectionGrid({ collections }) {
return (
<div className="grid gap-6 md:grid-cols-3">
{collections.map((collection, index) => (
<RFDS.ScrollReveal key={collection.id} animation="fade-up" delay={index * 100}>
<div className="rounded-2xl border border-white/10 p-6">
<h3>{collection.title}</h3>
<p>{collection.description}</p>
<RFDS.ButtonLink href={`/collections/${collection.handle}`} variant="ghost">
Shop the edit →
</RFDS.ButtonLink>
</div>
</RFDS.ScrollReveal>
))}
</div>
);
}- PascalCase:
ProductCard,ButtonLink - Descriptive:
LimitedDropsnotDrops - Noun-based:
ButtonnotClickable
- lowercase:
primary,secondary - Single word when possible:
ghostnotghost-button - Descriptive:
glass(describes effect)
- camelCase:
homeFeatured,dropNumber - Boolean prefix:
is,has,should(e.g.,isDrop,hasImage) - Event handlers:
onprefix (e.g.,onClick,onChange)
src/components/
├── rfds/
│ ├── index.ts # Main RFDS export
│ ├── primitives.ts # Primitive layer exports
│ ├── components.ts # Component layer exports
│ └── layouts.ts # Layout layer exports
│
├── ui/ # Primitive components
│ ├── button.tsx
│ ├── pill.tsx
│ ├── rating.tsx
│ ├── collapsible.tsx
│ ├── scroll-reveal.tsx
│ ├── product-card.tsx # Composed component
│ └── product-gallery.tsx
│
├── layout/ # Layout components
│ ├── header.tsx
│ └── footer.tsx
│
└── home/ # Page-specific components
├── hero.tsx
├── featured-look.tsx
└── ...
src/features/ # Feature modules (not in RFDS)
├── auth/
├── maintainer-progress/
└── impact/
RFDS is fully typed. TypeScript will autocomplete all available components and their props:
import { RFDS } from "@/components/rfds";
// TypeScript knows about all variants
<RFDS.Button variant="primary" | "secondary" | "ghost" | "glass" | "link" />
// Autocomplete for sizes
<RFDS.Button size="xs" | "sm" | "md" | "lg" />
// Type-safe props
<RFDS.ProductCard product={product} href={string} />Test components through the RFDS namespace:
import { RFDS } from "@/components/rfds";
import { render, screen } from "@testing-library/react";
it("renders primary button", () => {
render(<RFDS.Button variant="primary">Click</RFDS.Button>);
expect(screen.getByText("Click")).toBeInTheDocument();
});When adding new components to RFDS:
- Create component in appropriate directory
- Add to layer file (
primitives.ts,components.ts, etc.) - Add to main index (
rfds/index.ts) - Document in this file with examples
- Test via RFDS namespace
- Update version in header comment
Q: Should I use RFDS namespace or direct imports? A: RFDS namespace is recommended for consistency, but direct imports work too.
Q: Where do page-specific components go?
A: Keep them in src/components/home/, src/components/collections/, etc. Only add to RFDS if reusable across multiple pages.
Q: Can I add custom styles to RFDS components?
A: Yes, via className prop. But prefer using variants when possible.
Q: How do I add a new variant?
A: Edit the component file (e.g., button.tsx), add to variants object, update TypeScript types.
Last updated: October 2025 Design System Version: 1.1.0