Component Architecture: Reusable Button with Variants & Collapsible Accordion List
Production component design uses the Compound Component pattern, Class Variance Authority (CVA) for variants/sizes, forwardRef for DOM access, polymorphic `as` props, and strict WCAG ARIA keyboard accessibility.
Why it matters in interviews
Visual & Interactive Explanation
Component Architecture Design Patterns
Naive Prop Drilling
Monolithic mega-component
Compound & CVA Pattern
Industry Standard (Radix / MUI / Shadcn)
Code Examples & Implementation
import React, { forwardRef } from "react";
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
type ButtonSize = "sm" | "md" | "lg";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
as?: React.ElementType;
}
const variantStyles: Record<ButtonVariant, string> = {
primary: "bg-signal-cyan text-ink hover:brightness-110 active:scale-95 font-bold",
secondary: "bg-ink-raised text-text border border-ink-border hover:bg-ink-border",
outline: "border-2 border-signal-cyan text-signal-cyan hover:bg-signal-cyan/10",
ghost: "text-text-muted hover:text-text hover:bg-ink-raised",
danger: "bg-signal-red text-white hover:brightness-110 active:scale-95",
};
const sizeStyles: Record<ButtonSize, string> = {
sm: "px-2.5 py-1 text-xs rounded-md gap-1.5",
md: "px-4 py-2 text-sm rounded-lg gap-2",
lg: "px-6 py-3 text-base rounded-xl gap-2.5",
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "primary",
size = "md",
isLoading = false,
leftIcon,
rightIcon,
as: Component = "button",
className = "",
disabled,
children,
...props
},
ref
) => {
return (
<Component
ref={ref}
disabled={disabled || isLoading}
aria-busy={isLoading}
className={`inline-flex items-center justify-center font-medium transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
{...props}
>
{isLoading ? (
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
leftIcon
)}
<span>{children}</span>
{!isLoading && rightIcon}
</Component>
);
}
);
Button.displayName = "Button";import React, { useState, createContext, useContext, useId } from "react";
interface AccordionContextType {
openItems: Set<string>;
toggle: (id: string) => void;
allowMultiple?: boolean;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
export function Accordion({
children,
allowMultiple = false,
defaultOpenId,
}: {
children: React.ReactNode;
allowMultiple?: boolean;
defaultOpenId?: string;
}) {
const [openItems, setOpenItems] = useState<Set<string>>(
new Set(defaultOpenId ? [defaultOpenId] : [])
);
const toggle = (id: string) => {
setOpenItems((prev) => {
const next = new Set(allowMultiple ? prev : []);
if (prev.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggle, allowMultiple }}>
<div className="flex flex-col gap-2 rounded-xl border border-ink-border p-2">
{children}
</div>
</AccordionContext.Provider>
);
}
export function AccordionItem({
id,
title,
children,
}: {
id: string;
title: string;
children: React.ReactNode;
}) {
const ctx = useContext(AccordionContext);
if (!ctx) throw new Error("AccordionItem must be used within Accordion");
const contentId = useId();
const isOpen = ctx.openItems.has(id);
return (
<div className="rounded-lg border border-ink-border bg-ink-panel overflow-hidden">
<button
type="button"
onClick={() => ctx.toggle(id)}
aria-expanded={isOpen}
aria-controls={contentId}
className="flex w-full items-center justify-between px-4 py-3 text-left font-semibold text-sm text-text hover:text-signal-cyan transition"
>
<span>{title}</span>
<span className={`transition-transform duration-200 ${isOpen ? "rotate-180 text-signal-cyan" : ""}`}>
▾
</span>
</button>
{/* Smooth CSS Grid Zero-JS Height Transition */}
<div
id={contentId}
role="region"
className={`grid transition-all duration-200 ease-out ${
isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
}`}
>
<div className="overflow-hidden px-4 pb-4 pt-1 text-xs text-text-muted leading-relaxed">
{children}
</div>
</div>
</div>
);
}Interview-Ready Answers
Design reusable components using TypeScript generics for polymorphic rendering, CVA/Tailwind for variant mapping, React.forwardRef for DOM access, and strict ARIA attributes with keyboard navigation handlers.
Follow-up Questions & Deep Dives
🎯 Code Output — What Does This Print?
function Form() {
const handleSubmit = (e) => {
e.preventDefault();
console.log("Form submitted!");
};
return (
<form onSubmit={handleSubmit}>
<Button>Click Me</Button>
</form>
);
}
function Button({ children }: any) {
// Missing explicit type="button"
return <button>{children}</button>;
}
// When user clicks the button, does the form submit?Common Mistakes & Anti-Patterns
- Using non-semantic `<div>` or `<span>` for buttons and accordion triggers
- Measuring DOM `scrollHeight` with JavaScript on every render to animate accordion height
- Forgetting `type="button"` on buttons inside forms (default is `type="submit"`, causing unwanted form submissions)
- Not forwarding `ref` to the underlying DOM element
Implementation Evolution: Anti-Pattern to Production
❌ Anti-Pattern: Non-Semantic `<div onClick>` Collapsible Item
// ❌ Broken Anti-Pattern: Non-semantic div without ARIA or keyboard navigation
function BadAccordionItem({ title, content }: any) {
const [open, setOpen] = useState(false);
return (
<div onClick={() => setOpen(!open)}>
<div>{title}</div>
{open && <div>{content}</div>}
</div>
);
}✅ Correct Pattern: Semantic `<button>` with ARIA State & Region Role
// ✅ Correct: Semantic button, unique IDs for aria-controls, and role="region"
function GoodAccordionItem({ title, content, id }: any) {
const [open, setOpen] = useState(false);
const panelId = `panel-${id}`;
return (
<div>
<button
type="button"
aria-expanded={open}
aria-controls={panelId}
onClick={() => setOpen(!open)}
>
{title}
</button>
<div id={panelId} role="region" hidden={!open}>
{content}
</div>
</div>
);
}🚀 Production-Grade (Hardened): Full Design System Accordion with Arrow Key Roving Focus
// 🚀 Production: Roving focus (ArrowUp/Down, Home, End), Compound Component, ForwardRef
import React, { useRef, KeyboardEvent } from "react";
export function RovingAccordionList({ items }: { items: Array<{ id: string; title: string; content: string }> }) {
const listRef = useRef<HTMLDivElement>(null);
const [openId, setOpenId] = React.useState<string | null>(null);
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
const buttons = listRef.current?.querySelectorAll<HTMLButtonElement>("button[data-accordion-trigger]");
if (!buttons) return;
const btnArray = Array.from(buttons);
const currentIndex = btnArray.indexOf(document.activeElement as HTMLButtonElement);
if (e.key === "ArrowDown") {
e.preventDefault();
const next = (currentIndex + 1) % btnArray.length;
btnArray[next]?.focus();
} else if (e.key === "ArrowUp") {
e.preventDefault();
const prev = (currentIndex - 1 + btnArray.length) % btnArray.length;
btnArray[prev]?.focus();
} else if (e.key === "Home") {
e.preventDefault();
btnArray[0]?.focus();
} else if (e.key === "End") {
e.preventDefault();
btnArray[btnArray.length - 1]?.focus();
}
};
return (
<div ref={listRef} onKeyDown={handleKeyDown} className="flex flex-col gap-2">
{items.map((item) => {
const isOpen = openId === item.id;
return (
<div key={item.id} className="rounded-lg border border-ink-border bg-ink-panel p-3">
<button
data-accordion-trigger
aria-expanded={isOpen}
onClick={() => setOpenId(isOpen ? null : item.id)}
className="flex w-full justify-between font-semibold text-sm text-text"
>
{item.title}
<span>{isOpen ? "−" : "+"}</span>
</button>
<div
className={`grid transition-all duration-200 ${
isOpen ? "grid-rows-[1fr] mt-2 opacity-100" : "grid-rows-[0fr] opacity-0"
}`}
>
<div className="overflow-hidden text-xs text-text-muted">{item.content}</div>
</div>
</div>
);
})}
</div>
);
}⚠️ Trick Questions & Interviewer Traps
Q1:How do you animate an Accordion item height from 0 to 'auto' with pure CSS?
Q2:Why should you prefer `React.forwardRef` on foundational design system components?
📋 Rapid Revision Cheat Sheet
- CVA / Variant Matrix: Define variants (primary/outline) and sizes (sm/md/lg) cleanly.
- ForwardRef: Always wrap design system primitives in `React.forwardRef`.
- Zero-JS Height Animation: Use `grid-template-rows: 0fr -> 1fr` with `overflow: hidden`.
- ARIA Essentials: `aria-expanded`, `aria-controls`, `aria-busy`, and `role="region"`.
- Keyboard Nav: Support `ArrowDown`, `ArrowUp`, `Home`, and `End` for accordion accessibility.
- Polymorphism: Support `as` prop to render as `<a>`, `<button>`, or framework Links.
Real-World Architectural Scenario
In a checkout form, users repeatedly click 'Place Order' because the button has no loading state, resulting in double charges. How do you design the Button component to prevent this?
Rate Your Readiness
Rate how comfortably you can explain this in an interview to update your global readiness gauge: