3 Topics
Chapter•70 Minutes

Forms and Accessible UI Components

Build practical inputs, buttons, cards, navigation, dialogs, and accessible feedback patterns.

Inputs, labels, and validation

A polished form combines visual styling with native semantics. Use a real <label> connected to the input, a suitable input type, and clear helper or error text. Tailwind can style focus, invalid, disabled, and placeholder states, but it does not validate business rules or announce errors by itself.

Do not rely on placeholder text as the only label: it disappears when users type and may have poor contrast. Provide a visible label and make required fields and error messages clear.

Accessible input pattern
<div>
  <label for="task-title" class="block text-sm font-medium text-slate-800">Task title</label>
  <input id="task-title" name="title" required minlength="3"
    aria-describedby="task-title-help"
    class="mt-1.5 w-full rounded-lg border border-slate-300 px-3 py-2.5 text-slate-900 placeholder:text-slate-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
    placeholder="e.g. Review pull request">
  <p id="task-title-help" class="mt-1.5 text-sm text-slate-600">Use at least three characters.</p>
</div>
💡 The for and id connect the label to the input. Native required and minlength constraints provide baseline validation. In a full form, connect any error text and update it when validation runs.
Knowledge Check

Why should an input have a programmatically associated label?

Reusable buttons, cards, and navigation

Build components around clear semantic roles. Use <button> for actions and <a> for navigation. Cards should preserve heading order, meaningful links, and a logical reading order. A hover state should not be the only indication that something is interactive.

For navigation, indicate the current page with appropriate semantics such as aria-current="page". For icon-only buttons, provide an accessible name. Do not add ARIA roles that duplicate native semantics without a specific reason.

Navigation with current-page semantics
<nav aria-label="Primary navigation" class="flex flex-wrap gap-2">
  <a href="/overview" aria-current="page" class="rounded-lg bg-blue-50 px-3 py-2 text-sm font-semibold text-blue-800">Overview</a>
  <a href="/tasks" class="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900">Tasks</a>
  <a href="/settings" class="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900">Settings</a>
</nav>
💡 The current page is conveyed through aria-current, not only a background color. Links remain native navigation controls and are keyboard accessible by default.
Key Takeaways
  • Use buttons for actions and anchors for navigation.
  • Give icon-only controls accessible names.
  • Represent the current navigation item semantically.

Dialogs, overlays, and drawers

A dialog is more than a positioned box. It needs a sensible focus strategy, a way to close, keyboard behavior, and an appropriate accessible name. Use native <dialog> where it fits or a well-tested accessible dialog primitive. Tailwind handles the visual overlay and panel, but it does not trap focus or manage Escape behavior automatically.

Watch for clipping from ancestor overflow-hidden and stacking-context issues. A portal can help place overlays near the document root, but the component's behavior still needs to be implemented correctly.

Do not ship a visual-only dialog

A modal overlay that looks correct but allows keyboard focus to escape or leaves background controls interactive is incomplete. Use a tested accessible primitive or implement the full interaction contract.

Dialog visual structure (behavior omitted)
<div class="fixed inset-0 z-50 grid place-items-center bg-slate-950/50 p-4">
  <section role="dialog" aria-modal="true" aria-labelledby="confirm-title"
    class="w-full max-w-md rounded-2xl bg-white p-6 shadow-2xl">
    <h2 id="confirm-title" class="text-lg font-semibold text-slate-900">Delete this task?</h2>
    <p class="mt-2 text-sm leading-6 text-slate-600">This action cannot be undone.</p>
    <div class="mt-6 flex justify-end gap-3">
      <button class="rounded-lg border px-4 py-2 font-medium">Cancel</button>
      <button class="rounded-lg bg-red-600 px-4 py-2 font-semibold text-white">Delete task</button>
    </div>
  </section>
</div>
💡 This is a visual structure, not a complete accessible dialog implementation. Production code must manage opening/closing, initial focus, focus containment and restoration, Escape behavior, and background interaction according to the chosen dialog primitive.