112: Component Architecture, Headless Patterns, Styling, Animation, and Internationalization
Learning objectives
You will learn to:
- design reusable component APIs through composition;
- understand compound components;
- recognize render props and HOCs in existing code;
- build headless behavior separated from styling;
- choose styling approaches deliberately;
- use component libraries without losing semantics/accessibility;
- add motion without breaking reduced-motion preferences;
- design UI for localization rather than hard-coded English layout assumptions.
Component API design
A reusable component is a small library.
Its API should answer:
- what does the caller control?
- what does the component own?
- what are valid child structures?
- how are events reported?
- what accessibility responsibilities are built in?
Compound components
Example API:
<Tabs
value={tab}
onValueChange={
setTab
}
>
<Tabs.List
aria-label="Task views"
>
<Tabs.Trigger
value="open"
>
Open
</Tabs.Trigger>
<Tabs.Trigger
value="done"
>
Done
</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel
value="open"
>
<OpenTasks />
</Tabs.Panel>
<Tabs.Panel
value="done"
>
<CompletedTasks />
</Tabs.Panel>
</Tabs>
Compound APIs can make related pieces discoverable while still allowing composition.
Internally they often use Context.
Do not use compound components for every two-element component.
Headless components
A headless component/hook owns behavior and accessibility while letting consumers choose visual styling.
Examples:
- combobox behavior;
- menu keyboard interaction;
- dialog focus behavior;
- tabs selection model.
This is useful because sophisticated accessibility behavior is difficult to rebuild repeatedly.
Headless libraries should not be adopted blindly. Audit:
- keyboard model;
- ARIA semantics;
- bundle impact;
- portal behavior;
- controlled/uncontrolled support;
- styling integration.
Render props
You may encounter:
<DataLoader>
{({
data,
pending,
}) => (
...
)}
</DataLoader>
Render props were a common pre-Hooks composition mechanism and remain useful in some libraries.
Do not rewrite them merely because Hooks exist if the API remains clear and stable.
Higher-order components
Legacy/library code may contain:
const Enhanced =
withPermissions(
TaskPage,
);
An HOC receives a component and returns a component.
Modern application code often prefers Hooks/composition, but understanding HOCs is necessary for maintaining older React ecosystems.
Watch for:
- wrapper stacks;
- lost static properties;
- prop collisions;
- confusing DevTools trees.
Controlled component API
A reusable controlled component often uses:
value onValueChange
An uncontrolled version may use:
defaultValue
This mirrors browser control conventions.
Avoid trying to support both without a deliberate contract.
Slot/composition pattern
Rather than boolean options:
<Card
showHeader
showActions
showFooter
/>
prefer composition:
<Card>
<Card.Header>
Tasks
</Card.Header>
<TaskList />
<Card.Footer>
<TaskCount />
</Card.Footer>
</Card>
This reduces prop explosion.
Styling options
React does not require one styling system.
Common choices:
Plain CSS
Strong baseline:
import './TaskCard.css';
CSS Modules
Scoped class names:
import styles
from './TaskCard.module.css';
function TaskCard() {
return (
<article
className={
styles.card
}
>
...
</article>
);
}
Utility CSS
Libraries such as Tailwind can make design tokens and responsive utility composition convenient.
Trade-offs include class-heavy markup and tooling conventions.
CSS-in-JS
Useful in some design systems, but evaluate:
- runtime cost;
- server rendering behavior;
- style insertion;
- compiler/build integration.
Do not choose based on trend alone.
Component libraries
Libraries can accelerate:
- accessible primitives;
- complex form controls;
- data display;
- theming.
But a library does not replace product semantics.
Audit generated DOM.
If a visual “button” renders a non-interactive <div>, the abstraction is wrong for accessibility.
Styling state
Prefer state reflected through attributes/classes:
<button
aria-pressed={
selected
}
data-state={
selected
? 'selected'
: 'idle'
}
>
CSS:
button[data-state="selected"] {
font-weight: 700;
}
This keeps visual rules in CSS and state rules in React.
Animation
Start with CSS for simple transitions.
Use a React animation library when you need:
- enter/exit orchestration;
- layout animations;
- gesture animation;
- spring physics;
- complex coordinated sequences.
Do not animate purely because a library is installed.
Reduced motion
Respect:
@media (
prefers-reduced-motion:
reduce
) {
*,
*::before,
*::after {
animation-duration:
0.001ms !important;
animation-iteration-count:
1 !important;
scroll-behavior:
auto !important;
}
}
For important JavaScript-driven motion, detect the preference through an appropriate media-query abstraction.
Reduced motion means reducing unnecessary motion, not removing all visual feedback.
Accessibility architecture
Build accessibility into component contracts.
Button:
<button
type="button"
aria-pressed={
active
}
>
Field:
<label htmlFor={id}> ... </label>
Dialog:
- accessible name;
- focus entry;
- focus return;
- dismiss behavior;
- background interaction handling.
Do not add ARIA where native HTML already provides the correct semantics.
Internationalization
Hard-coded string concatenation causes localization problems:
<p>
You have
{' '}
{count}
{' '}
tasks
</p>
Plural rules differ across languages.
Use an i18n formatter/library that supports message pluralization.
Also design for:
- longer translated strings;
- right-to-left layout;
- locale-sensitive dates/numbers;
- time zones;
- language changes.
Use Intl for locale-sensitive primitives where appropriate.
Component ownership and design system boundaries
A design system component should not import business API code.
Bad:
shared/ui/Button → imports taskApi
Good:
feature task action → composes shared Button
Dependencies should point from feature code toward reusable primitives, not the other way around.
Common mistakes
- giant configurable components with dozens of boolean props;
- rebuilding complex ARIA widgets from scratch without understanding keyboard patterns;
- mixing server calls into shared UI primitives;
- styling every state inline;
- assuming English text length;
- animation that ignores reduced-motion preference;
- using divs as buttons;
- adopting multiple styling systems without design rules.
Exercises
- Build controlled Tabs with compound components.
- Convert a prop-heavy Card to composition.
- Style a component using CSS Modules.
- Audit a headless dialog for focus and accessible name.
- Add reduced-motion behavior.
- Format task dates using locale-aware
Intl.DateTimeFormat. - Draw a dependency diagram for shared UI versus feature code.
Exit questions
- What makes a component “headless”?
- Why are compound components useful?
- What are render props and HOCs?
- How should styling-system choice be evaluated?
- Why is reduced motion a component responsibility?
- Why should design-system components not import business APIs?
Official references
- https://react.dev/learn/passing-props-to-a-component
- https://react.dev/learn/passing-data-deeply-with-context
- https://react.dev/reference/react-dom/components/common
- https://www.w3.org/WAI/ARIA/apg/
Deep dive: reusable component design is about invalid states
A good component API makes common valid use easy and invalid combinations difficult.
Poor:
<Modal
open
closed={false}
hasHeader
noHeader={false}
dismissable
noOverlay={false}
type="confirmation"
destructive
/>
Too many overlapping switches.
Better:
<Dialog open={open} onOpenChange={setOpen}>
<Dialog.Content>
<Dialog.Title>Delete task?</Dialog.Title>
<Dialog.Description>
This cannot be undone.
</Dialog.Description>
<Dialog.Actions>
<Button variant="ghost">Cancel</Button>
<Button variant="danger">Delete</Button>
</Dialog.Actions>
</Dialog.Content>
</Dialog>
Composition constrains structure through meaningful parts.
Polymorphic components
Design systems sometimes support:
<Button asChild>
<Link to="/tasks">Tasks</Link>
</Button>
or an as prop.
Be careful.
Changing host element changes:
- semantics;
- keyboard behavior;
- required props;
- ref type;
- accessibility.
Do not make everything polymorphic by default.
A <button> and <a> are not interchangeable merely because they look alike.
Headless state machines
A headless menu may manage:
closed open active item keyboard navigation typeahead focus return outside click escape portal disabled items
That is substantial behavior.
Using Radix, React Aria, Headless UI, Ark UI, or similar mature primitives can be safer than reimplementing complex ARIA patterns.
But audit the specific library/version and understand its DOM contract.
Component library evaluation checklist
Before adopting:
- React 19 compatibility;
- SSR/hydration behavior;
- accessibility claims/evidence;
- bundle/tree shaking;
- controlled/uncontrolled APIs;
- styling model;
- portal layering;
- RTL support;
- form integration;
- animation/reduced motion;
- maintenance/security health.
Do not choose based only on screenshot appearance.
Styling architecture
Plain/global CSS
Good for:
- design tokens;
- layout primitives;
- small apps.
Risk:
- global naming collisions without conventions.
CSS Modules
Good for local component styles.
import styles from './TaskCard.module.css';
Utility classes
Good for consistent token-based composition and rapid UI.
Need conventions for:
- long conditional class lists;
- reusable variants;
- design tokens.
CSS-in-JS
Evaluate runtime versus build-time approaches.
Runtime style insertion can interact with:
- SSR;
- streaming;
- style ordering;
useInsertionEffect.
Modern tooling may extract styles at build time.
Variant management
Instead of string concatenation spread across components, design reusable variant helpers if utility styling is used.
Example conceptual API:
buttonClass({
variant: 'danger',
size: 'sm',
loading: true,
});
Keep visual variant logic separate from business authorization.
Button hidden/disabled due permission is business UI state; color variant is design-system state.
Design tokens
Use CSS variables:
:root {
--color-surface: ...;
--color-danger: ...;
--space-2: .5rem;
--radius-md: .5rem;
}
React should not own static token state.
Theme switching can toggle an attribute/class:
<html data-theme="dark">
CSS handles values.
Animation and presence
Animating mount/unmount is harder than static transitions because the DOM node disappears.
Animation libraries may provide presence primitives.
Architecture questions:
- should exit animation delay unmount?
- what happens if state flips during exit?
- focus if dialog is closing?
- reduced motion?
- route navigation interruption?
Motion is behavior, not decoration only.
View Transitions and React ecosystem
Modern browsers/React ecosystem increasingly expose view-transition capabilities.
Use progressive enhancement.
Do not make navigation correctness depend on an animation API.
Reduced-motion users should receive a stable experience.
Internationalization details
Never store formatted display strings as canonical data.
Store:
ISO timestamp / numeric value / message key/domain state
Format at presentation:
new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'short',
}).format(date);
Currency:
new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
Do not hard-code:
₹${amount}
if product supports multiple locales/currencies.
RTL
Use logical CSS:
margin-inline-start
padding-inline
inset-inline-end
instead of always left/right.
Test icons whose direction carries meaning:
- back arrow;
- next chevron.
Some icons mirror in RTL; others (play, logos) may not.
Composition and data dependencies
Shared UI component should receive data/behavior, not fetch business resource:
Bad:
function UserAvatar({ userId }) {
const query = useQuery(...);
}
for a design-system Avatar.
Better:
<Avatar
src={user.avatarUrl}
name={user.name}
/>
A feature-specific:
<UserAvatar userId={...} />
may wrap data loading if that is a deliberate feature component.
Name boundaries clearly.
Compound Context performance
Compound components often use Context:
<TabsContext value={...}>
If context contains rapidly changing large state, all consumers may rerender.
For complex component libraries, consider split contexts or external-store patterns.
Do not prematurely optimize small component sets.
Escape hatches in component APIs
Some components need refs:
<TextField ref={fieldRef} />
Modern React 19 supports ref as prop.
Expose imperative APIs only where declarative props cannot express needed behavior.
Error and loading slots
Reusable data panels can accept presentation slots:
<DataPanel
loading={<PanelSkeleton />}
error={(error) => <PanelError error={error} />}
>
...
</DataPanel>
But avoid building a generic "handles every async thing" component that duplicates TanStack Query/Suspense semantics.
Failure clinic
div with onClick styled as button
Semantics/keyboard broken.
Shared UI fetches feature API
Dependency inversion broken.
Every component accepts className and arbitrary props without policy
Can be useful, but design-system API becomes uncontrolled. Decide extension strategy.
Hard-coded English width
Translated UI overflows.
Animation ignores focus
Focus lands on unmounted/hidden nodes.
Exercises
- Redesign an invalid-state-heavy modal API.
- Build accessible Tabs compound API.
- Compare one headless library primitive against WAI-ARIA APG.
- Theme with CSS custom properties.
- Add locale-aware currency/date.
- Test layout in RTL and with 2× text.
- Audit an exit animation for focus/reduced motion.
- Draw dependency boundary between design system and feature API.
Mastery check
Explain:
- invalid-state API design;
- headless behavior;
- styling-system trade-offs;
- design tokens;
- motion lifecycle;
- i18n/RTL concerns;
- data dependency direction.
Production case study: designing a reusable DataTable boundary
A DataTable can easily become a 60-prop monster.
Avoid mixing:
query fetching pagination API row selection column rendering permissions CSV export modal state styling
into one universal component.
A better split:
OrdersTableFeature ├─ owns Query + URL + permissions └─ composes DataTable ├─ columns ├─ rows ├─ selection callbacks └─ presentational states
Generic component:
<DataTable
columns={columns}
rows={orders}
getRowKey={(order) => order.id}
selectedKeys={selectedIds}
onSelectionChange={setSelectedIds}
/>
Feature owns:
where rows came from what selected orders mean whether export is allowed which route opens
This keeps the design-system component reusable without making it a hidden application framework.
Additional depth: React ecosystem choices without turning the course into library memorization
The React roadmap includes many optional ecosystem branches.
You should recognize categories and evaluate them.
Component systems
Examples in the ecosystem include:
MUI Chakra UI Mantine Ant Design
They provide styled components/design systems.
Headless primitives
Examples:
Radix React Aria Headless UI Ark UI
They emphasize behavior/accessibility primitives.
Animation
Examples:
Motion GSAP React Spring
Choose based on interaction complexity and bundle/runtime needs.
Frameworks
Examples:
Next.js React Router Framework Mode Astro with React islands
Frameworks add rendering/routing/server/deployment architecture.
Mobile
React Native uses React's component/state mental model but different host platform and component primitives.
Knowing React DOM does not mean knowing React Native layout, native modules, navigation, performance, or platform UX.
GraphQL
Apollo/urql and other clients may own GraphQL server cache.
If a project standardizes TanStack Query for REST, do not add Apollo merely because GraphQL exists. GraphQL architecture can justify a GraphQL-aware client.
The course teaches categories and decision criteria; specialist libraries should become separate advanced modules when the project actually uses them.
