Lesson 36 of 45 intermediate

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?

Open interactive version (quiz + challenge)

Real-world analogy

A good kitchen drawer has a few well-labelled tools: a knife, a whisk, a spoon. Each does one job, and you can use it in any recipe. A drawer with one giant gadget with forty dials is hard to use and breaks easily. If a shop sells you the whole drawer, you add your own labels. Then changing the supplier does not change how you cook. The tools are components, the labels are the wrapper around a UI library, and the dials are config props.

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

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. 1. ButtonProps adds two own props, variant and loading, to all the native button props.
  2. 2. Button picks out its own props and a default type of button, which avoids accidental form submits. The rest is forwarded.
  3. 3. The button is disabled while loading and sets aria-busy, so assistive tools know it is working.
  4. 4. TextFieldProps adds a label and an optional error to the native input props.
  5. 5. useId makes a unique id, and htmlFor links the label to the input, so clicking the label focuses the field.
  6. 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?
Can a person using only a keyboard reach or activate this element?
Show answer
A div is not focusable and has no button role, so Tab skips it and Enter or Space do nothing, and screen readers do not announce it as a button. Fix: use a real button element, <button type="button" onClick={onSave}>.

Explain like I'm 5

A good toy box has a few simple toys that anyone can use. A real button works for everyone, including people who cannot use a mouse. A label is a name tag that tells everyone what a box is for.

Fun fact

A real button gives you keyboard focus, the Enter and Space keys, and a role for screen readers for free. A div gives you none of them.

Hands-on challenge

Say it out loud in 30 seconds: "How do you build reusable, accessible components?" Then check: did you mention a small prop API, children for composition, wrapping a UI library, semantic elements and labels, and keyboard focus?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook