Skip to content
GeeksSmith
intermediate 9 min read

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

Asked directly in Tekion Corp Round 1 ('Build a React list with collapse/expand' and 'Create a reusable Button component with variants and sizes like MUI'). Interviewers test clean API ergonomics, ref forwarding, TypeScript generics, compound state, and keyboard navigation.

Visual & Interactive Explanation

Component Architecture Design Patterns

Interactive Comparison

Naive Prop Drilling

Monolithic mega-component

Fragile
FlexibilityRigid; needs props for every tiny style change
Bundle SizeBloated; imports unused variant styles
AccessibilityOften misses ARIA linkages and keyboard traps

Compound & CVA Pattern

Industry Standard (Radix / MUI / Shadcn)

Production Standard
FlexibilityComposable subcomponents with React Context
PolymorphismRenders as <a>, <button>, or <Link> safely
AccessibilityAutomatic aria-expanded & Arrow key navigation

Code Examples & Implementation

Production Reusable Button with Variants & Sizes (MUI/CVA Pattern)
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";
Accessible Collapsible Accordion List with Keyboard Navigation
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?

Question 1medium
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>
  );
}
Why this breaks: A `<div onClick>` cannot be focused using the Tab key, does not respond to Space or Enter, lacks `aria-expanded` state for screen readers, and causes layout jumps instead of smooth CSS transitions.

✅ 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:

Rate your confidence: