Reusable Components, UI Libraries and Accessibility
Small prop APIs, wrapped libraries, and markup that works without a mouse
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
Why wrap a UI library component in your own Button component?
Real-world analogy
What is it?
A reusable component is a piece of UI with a small, typed interface. Many screens can use it without knowing its inside. Good ones forward native props, accept children, and show every state a designer asked for. Accessibility is part of that interface. It means the right element, a linked label, keyboard focus and clear errors. Then keyboard and screen reader users can use it too.
Real-world relevance
A team builds a form with five fields. A shared TextField takes a label and an error and links them to the input. Each form that uses it gets linked labels. Later the team swaps the styling library. Because the screens use their own Button and TextField, only two files change. A tester who only uses the keyboard finds a clickable div that Tab cannot reach. This is a common accessibility bug.
Key points
- Small, clear prop API — A reusable component takes a few props with clear names, such as variant and loading. It has no business logic, so it is easy to use and to test.
- Compose, do not configure — Accept children or slots for content, instead of a prop for every variation. A card with a header slot is simpler than a card with ten text props.
- Controlled and uncontrolled — A controlled input gets its value from state and reports changes with onChange. An uncontrolled one keeps its own value in the DOM and you read it when needed.
- Forward native props — Spread the rest of the native props onto the element, typed with ButtonHTMLAttributes. Callers can then pass onClick, disabled or aria-label without you listing them.
- UI libraries save time — MUI, Ant Design and shadcn/ui give tested components and often good accessibility. They come with their own look, size and upgrade path.
- Wrap the library — Import the library in one place inside your own Button or Modal. If you change libraries later, you edit the wrapper, not every screen.
- From design spec to code — Turn colours and spacing into tokens, and build each state the designer shows: hover, focus, disabled, loading, empty and error. Missing states are a common review comment.
- Accessibility basics — Use semantic elements such as button, nav and label. Add visible focus, enough contrast, and use aria-* only when native HTML cannot express the meaning.
- Responsive and cross-browser — Check the layout at a narrow phone width and at a wide desktop width. Try the main flow in at least Chrome, Safari and Firefox.
Quick check
Answer from memory — scrolling up first makes it stick less.
What makes an input controlled?
What does htmlFor on a label do?
Code example
import { useId, type ButtonHTMLAttributes, type InputHTMLAttributes } from 'react';
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & { variant?: 'primary' | 'secondary'; loading?: boolean };
export function Button({
variant = 'primary', loading = false, type = 'button', disabled, children, ...rest
}: ButtonProps) {
return (
<button
{...rest}
type={type}
className={'btn btn-' + variant}
disabled={disabled || loading}
aria-busy={loading}
>
{loading ? 'Loading...' : children}
</button>
);
}
type TextFieldProps = InputHTMLAttributes<HTMLInputElement> & { label: string; error?: string };
export function TextField({ label, error, ...rest }: TextFieldProps) {
const id = useId();
const errorId = id + '-error';
return (
<div>
<label htmlFor={id}>{label}</label>
<input
{...rest} id={id}
aria-invalid={error ? true : undefined}
aria-describedby={error ? errorId : undefined}
/>
{error && <p id={errorId} role="alert">{error}</p>}
</div>
);
}Line-by-line walkthrough
- 1. ButtonProps adds two own props, variant and loading, to all the native button props.
- 2. Button picks out its own props and a default type of button, which avoids accidental form submits. The rest is forwarded.
- 3. The button is disabled while loading and sets aria-busy, so assistive tools know it is working.
- 4. TextFieldProps adds a label and an optional error to the native input props.
- 5. useId makes a unique id, and htmlFor links the label to the input, so clicking the label focuses the field.
- 6. When there is an error, aria-describedby points at the message, and role alert makes screen readers announce it.
Spot the bug
type Props = { onSave: () => void };
function SaveButton({ onSave }: Props) {
return (
<div className="btn" onClick={onSave}>
Save
</div>
);
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Accessibility on the web (MDN)
- The input component in React DOM (React)
- Free Full-Stack Playbook (React and TypeScript lessons) (Teamz Lab)
Lock it in