Tailwind Alert: 17 Examples of Alerts, Banners and Toasts

Tailwind has no alert class, so you build the box yourself from a flex container, padding, a border and a tinted background. The markup is where people trip: MDN reserves role="alert" for content that appears dynamically, yet popular kits put it on static markup.

Below are 17 Tailwind alert examples you can copy, followed by a guide to roles, dismissal and dark mode.

By Bogdan Sandu ยท Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Status alerts, styles and tokens

The four states in different treatments: soft tinted boxes, accent borders, a CSS-only style switcher, prefixed @theme tokens with a dark toggle and explicit dark: pairs. Every alert pairs its color with an icon shape and a written title, so the state never depends on color alone.

Success, info, warning and error

  • bg-green-50
  • border-sky-200
  • shrink-0
  • sr-only

One structure, four color families: a flex row with gap-3, a 50 fill, a 200 border, 900 titles and 800 body text, which all clear 4.5:1. Each state has its own icon shape and a hidden Success:, Info:, Warning: or Error: prefix, so nothing depends on color. These boxes are on the page at load, so they carry no live role; the live region demo further down shows how to announce a message that appears later.

View code
<div class="grid w-full max-w-lg gap-3">
  <div class="flex gap-3 rounded-xl border border-green-200 bg-green-50 p-4 text-sm">
    <svg class="size-5 shrink-0 text-green-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
    <div class="min-w-0">
      <p class="font-semibold text-green-900"><span class="sr-only">Success: </span>Payment received</p>
      <p class="mt-1 text-green-800">Invoice INV-2048 for $1,240.00 was paid on October 28. The receipt went to billing@example.com.</p>
    </div>
  </div>
  <div class="flex gap-3 rounded-xl border border-sky-200 bg-sky-50 p-4 text-sm">
    <svg class="size-5 shrink-0 text-sky-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 11v5m0-8.5v.01"/></svg>
    <div class="min-w-0">
      <p class="font-semibold text-sky-900"><span class="sr-only">Info: </span>Scheduled maintenance</p>
      <p class="mt-1 text-sky-800">The dashboard is read-only on Sunday, November 2, from 02:00 to 03:00 UTC.</p>
    </div>
  </div>
  <div class="flex gap-3 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm">
    <svg class="size-5 shrink-0 text-amber-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
    <div class="min-w-0">
      <p class="font-semibold text-amber-900"><span class="sr-only">Warning: </span>Storage almost full</p>
      <p class="mt-1 text-amber-800">You have used 9.2 GB of 10 GB. New uploads stop at the limit.</p>
    </div>
  </div>
  <div class="flex gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm">
    <svg class="size-5 shrink-0 text-red-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
    <div class="min-w-0">
      <p class="font-semibold text-red-900"><span class="sr-only">Error: </span>Card declined</p>
      <p class="mt-1 text-red-800">The card ending in 4242 was declined by the bank. Use another card or contact the issuer.</p>
    </div>
  </div>
</div>

Accent border alerts

  • border-s-4
  • rounded-e-xl
  • marker:
  • ring-1

The accent is border-s-4, a logical property, so the thick edge moves to the right in a right-to-left layout, and rounded-e-xl rounds only the opposite corners. The middle alert keeps a white fill with ring-1 for a calmer look, and marker:text-orange-500 tints the list bullets without touching the text.

View code
<div class="grid w-full max-w-lg gap-4">
  <div class="rounded-e-xl border-s-4 border-orange-500 bg-orange-50 p-4 text-sm text-orange-950">
    <div class="flex gap-3">
      <svg class="size-5 shrink-0 text-orange-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
      <div class="min-w-0">
        <p class="font-semibold"><span class="sr-only">Warning: </span>Password rules change on December 1</p>
        <ul class="mt-2 list-disc space-y-1 ps-5 text-orange-900 marker:text-orange-500">
          <li>At least 12 characters</li>
          <li>No reuse of your last 5 passwords</li>
          <li>Members who have not updated are asked at their next sign-in</li>
        </ul>
        <a href="#" class="mt-3 inline-block rounded-sm font-semibold text-orange-800 underline underline-offset-4 hover:text-orange-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700">Review the new policy</a>
      </div>
    </div>
  </div>
  <div class="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-e-xl border-s-4 border-indigo-500 bg-white p-4 text-sm text-slate-700 shadow-xs ring-1 ring-slate-200">
    <svg class="size-5 shrink-0 text-indigo-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 11v5m0-8.5v.01"/></svg>
    <p class="min-w-0 grow basis-52"><span class="sr-only">Info: </span>Two-factor sign-in is on for <span class="font-semibold text-slate-900">18 of 24</span> members.</p>
    <a href="#" class="rounded-sm font-semibold text-indigo-700 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Remind the other 6</a>
  </div>
  <div class="flex gap-3 rounded-e-xl border-s-4 border-rose-600 bg-rose-50 p-4 text-sm">
    <svg class="size-5 shrink-0 text-rose-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
    <div class="min-w-0">
      <p class="font-semibold text-rose-950"><span class="sr-only">Error: </span>SSO certificate expired on October 27</p>
      <p class="mt-1 text-rose-900">Members who use single sign-on see an error until an admin uploads a new certificate.</p>
    </div>
  </div>
</div>

Soft, solid and outline styles

  • @custom-variant
  • :has()
  • has-checked:
  • has-focus-visible:

Pick a style above the alerts.

Two @custom-variant lines turn a checked radio into variants: tal-solid: and tal-line: match when the wrapper has that radio checked, so each alert lists its soft, solid and outline colors side by side and no JavaScript runs. The segmented control styles its labels with has-checked: and has-focus-visible:. Solid teal and rose use the 700 shades and amber stays light with dark text, which keeps all three above 4.5:1.

View code
<div class="tal-sw w-full max-w-lg">
  <fieldset>
    <legend class="text-sm font-medium text-slate-700">Alert style</legend>
    <div class="mt-2 inline-flex rounded-xl bg-slate-200/70 p-1">
      <label id="tal-sw-l-soft" class="cursor-pointer rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-slate-900 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-slate-900"><input type="radio" name="tal-sw" id="tal-sw-soft" value="soft" class="sr-only" checked>Soft</label>
      <label id="tal-sw-l-solid" class="cursor-pointer rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-slate-900 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-slate-900"><input type="radio" name="tal-sw" id="tal-sw-solid" value="solid" class="sr-only">Solid</label>
      <label id="tal-sw-l-line" class="cursor-pointer rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-slate-900 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-slate-900"><input type="radio" name="tal-sw" id="tal-sw-line" value="outline" class="sr-only">Outline</label>
    </div>
  </fieldset>
  <div class="mt-4 grid gap-3">
    <div class="flex gap-3 rounded-xl border border-teal-200 bg-teal-50 p-4 text-sm text-teal-900 tal-solid:border-teal-700 tal-solid:bg-teal-700 tal-solid:text-white tal-line:border-teal-600 tal-line:bg-white tal-line:text-teal-800">
      <svg class="size-5 shrink-0 text-teal-700 tal-solid:text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
      <p><span class="font-semibold"><span class="sr-only">Success: </span>Backup complete.</span> 14,203 files copied to the Frankfurt region in 6 minutes.</p>
    </div>
    <div class="flex gap-3 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950 tal-solid:border-amber-400 tal-solid:bg-amber-400 tal-solid:text-amber-950 tal-line:border-amber-500 tal-line:bg-white tal-line:text-amber-900">
      <svg class="size-5 shrink-0 text-amber-700 tal-solid:text-amber-950" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
      <p><span class="font-semibold"><span class="sr-only">Warning: </span>Backup is slow.</span> The last three runs took over an hour. Check the upload limit.</p>
    </div>
    <div class="flex gap-3 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900 tal-solid:border-rose-700 tal-solid:bg-rose-700 tal-solid:text-white tal-line:border-rose-600 tal-line:bg-white tal-line:text-rose-800">
      <svg class="size-5 shrink-0 text-rose-600 tal-solid:text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
      <p><span class="font-semibold"><span class="sr-only">Error: </span>2 folders skipped.</span> /design/raw and /video/exports were locked by another app.</p>
    </div>
  </div>
</div>

Status tokens with @theme

  • @theme inline
  • bg-tal-ok/10
  • border-tal-err/30
  • aria-pressed

Press Dark to swap the token values.

Six prefixed tokens in @theme inline point at CSS variables, so Tailwind generates bg-tal-ok, text-tal-warn, border-tal-err and the rest, and opacity modifiers such as bg-tal-ok/10 mix the tint for you. The dark values live on a .dark ancestor, so the markup carries no dark: classes at all. The JS only toggles the dark class on the wrapper and aria-pressed on the button.

View code
<div id="tal-tok-wrap" class="w-full max-w-md">
  <div class="tal-tok rounded-2xl border border-tal-ink/10 bg-tal-card p-5 text-tal-ink shadow-sm">
    <div class="flex items-center justify-between gap-3">
      <div class="min-w-0">
        <h3 class="font-semibold">Release 3.8 checks</h3>
        <p class="text-sm text-tal-ink/70">main, commit 4f2a9c1</p>
      </div>
      <button type="button" id="tal-tok-mode" aria-pressed="false" class="inline-flex min-h-11 shrink-0 items-center gap-2 rounded-lg border border-tal-ink/15 px-3.5 text-sm font-medium hover:bg-tal-ink/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tal-info">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5Z"/></svg>
        Dark
      </button>
    </div>
    <ul class="mt-4 grid gap-2.5">
      <li class="flex gap-3 rounded-lg border border-tal-ok/30 bg-tal-ok/10 p-3 text-sm">
        <svg class="size-5 shrink-0 text-tal-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
        <p><span class="font-semibold text-tal-ok">Tests passed.</span> 412 of 412 in 3 min 10 s.</p>
      </li>
      <li class="flex gap-3 rounded-lg border border-tal-info/30 bg-tal-info/10 p-3 text-sm">
        <svg class="size-5 shrink-0 text-tal-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 11v5m0-8.5v.01"/></svg>
        <p><span class="font-semibold text-tal-info">Preview ready.</span> Deployed to staging-3 for review.</p>
      </li>
      <li class="flex gap-3 rounded-lg border border-tal-warn/30 bg-tal-warn/10 p-3 text-sm">
        <svg class="size-5 shrink-0 text-tal-warn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
        <p><span class="font-semibold text-tal-warn">Bundle grew.</span> app.js is 18 kB larger than in 3.7.</p>
      </li>
      <li class="flex gap-3 rounded-lg border border-tal-err/30 bg-tal-err/10 p-3 text-sm">
        <svg class="size-5 shrink-0 text-tal-err" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
        <p><span class="font-semibold text-tal-err">Migration failed.</span> 0042_add_invoice_index timed out after 30 s.</p>
      </li>
    </ul>
  </div>
</div>

Dark mode alert pairs

  • dark:bg-emerald-950/50
  • dark:border-red-800/70
  • sm:grid-cols-2
  • tabular-nums

The opposite approach to tokens: every alert spells out its dark pair inline, a 950 fill at 50% opacity, a 800 border at 70% and 100 or 200 text, so the tint stays visible on a slate-900 card without glowing. With the @custom-variant line from the setup box, these classes switch when a dark class sits on any ancestor.

View code
<div class="w-full max-w-2xl rounded-2xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
  <div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
    <h3 class="font-semibold text-slate-900 dark:text-white">Cluster health, eu-west</h3>
    <p class="text-sm text-slate-500 tabular-nums dark:text-slate-400">Updated 14:32 UTC</p>
  </div>
  <div class="mt-4 grid gap-3 sm:grid-cols-2">
    <div class="flex gap-3 rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900 dark:border-emerald-800/70 dark:bg-emerald-950/50 dark:text-emerald-100">
      <svg class="size-5 shrink-0 text-emerald-700 dark:text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
      <div class="min-w-0"><p class="font-semibold"><span class="sr-only">Success: </span>eu-west-2 recovered</p><p class="mt-1 text-emerald-800 dark:text-emerald-200/90">All 12 nodes are healthy again after the 09:10 restart.</p></div>
    </div>
    <div class="flex gap-3 rounded-xl border border-cyan-200 bg-cyan-50 p-4 text-sm text-cyan-900 dark:border-cyan-800/70 dark:bg-cyan-950/50 dark:text-cyan-100">
      <svg class="size-5 shrink-0 text-cyan-700 dark:text-cyan-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 11v5m0-8.5v.01"/></svg>
      <div class="min-w-0"><p class="font-semibold"><span class="sr-only">Info: </span>Autoscaling is on</p><p class="mt-1 text-cyan-800 dark:text-cyan-200/90">The pool grows to 20 nodes when CPU stays above 70% for 5 minutes.</p></div>
    </div>
    <div class="flex gap-3 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/50 dark:text-amber-100">
      <svg class="size-5 shrink-0 text-amber-700 dark:text-amber-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
      <div class="min-w-0"><p class="font-semibold"><span class="sr-only">Warning: </span>Latency above target</p><p class="mt-1 text-amber-800 dark:text-amber-200/90">p95 for /search is <span class="tabular-nums">820 ms</span>. The target is 500 ms.</p></div>
    </div>
    <div class="flex gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900 dark:border-red-800/70 dark:bg-red-950/50 dark:text-red-100">
      <svg class="size-5 shrink-0 text-red-600 dark:text-red-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
      <div class="min-w-0"><p class="font-semibold"><span class="sr-only">Error: </span>Queue worker stalled</p><p class="mt-1 text-red-800 dark:text-red-200/90"><span class="tabular-nums">4,180</span> jobs have waited since 14:05. Restart the worker or scale it out.</p></div>
    </div>
  </div>
</div>

Dismissible alerts, actions and details

Alerts the user can act on: close buttons that hand focus to the next sensible element, Retry and Later buttons, and a build error that tucks its log into a native details element.

Dismissible notices with focus handoff

  • motion-safe:transition-[opacity,scale]
  • hidden attribute
  • tabindex="-1"
  • role="status"

Use the buttons inside the alert.

Each close button has type="button" and an aria-label that names its notice. When a notice goes, focus moves to the next close button, or to the section heading once the list is empty, so keyboard users never land on the body. The fade uses motion-safe:transition-[opacity,scale], then the hidden attribute removes the item, which wins over flex in Tailwind v4.

View code
<section aria-labelledby="tal-dis-title" class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
  <div class="flex flex-wrap items-center justify-between gap-2">
    <h3 id="tal-dis-title" tabindex="-1" class="rounded-sm font-semibold text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Workspace notices</h3>
    <p id="tal-dis-count" role="status" class="text-sm text-slate-500">3 notices</p>
  </div>
  <ul class="mt-4 grid gap-3">
    <li class="tal-dis-item flex gap-3 rounded-xl border border-sky-200 bg-sky-50 p-4 text-sm text-sky-900 motion-safe:transition-[opacity,scale] motion-safe:duration-200">
      <svg class="size-5 shrink-0 text-sky-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 11v5m0-8.5v.01"/></svg>
      <div class="min-w-0 flex-1"><p class="font-semibold"><span class="sr-only">Info: </span>Shared calendars are here</p><p class="mt-1 text-sky-800">Teams can now book meeting rooms from one calendar.</p></div>
      <button type="button" aria-label="Dismiss: Shared calendars are here" class="tal-dis-close -m-1.5 grid size-9 shrink-0 place-items-center rounded-lg text-sky-700 hover:bg-sky-100 hover:text-sky-950 focus-visible:outline-2 focus-visible:outline-sky-700">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </li>
    <li class="tal-dis-item flex gap-3 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 motion-safe:transition-[opacity,scale] motion-safe:duration-200">
      <svg class="size-5 shrink-0 text-amber-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
      <div class="min-w-0 flex-1"><p class="font-semibold"><span class="sr-only">Warning: </span>23 of 25 seats used</p><p class="mt-1 text-amber-800">Add seats before you invite more people.</p></div>
      <button type="button" aria-label="Dismiss: 23 of 25 seats used" class="tal-dis-close -m-1.5 grid size-9 shrink-0 place-items-center rounded-lg text-amber-700 hover:bg-amber-100 hover:text-amber-950 focus-visible:outline-2 focus-visible:outline-amber-700">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </li>
    <li class="tal-dis-item flex gap-3 rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900 motion-safe:transition-[opacity,scale] motion-safe:duration-200">
      <svg class="size-5 shrink-0 text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
      <div class="min-w-0 flex-1"><p class="font-semibold"><span class="sr-only">Success: </span>Import finished</p><p class="mt-1 text-emerald-800">1,204 contacts were added from contacts-oct.csv.</p></div>
      <button type="button" aria-label="Dismiss: Import finished" class="tal-dis-close -m-1.5 grid size-9 shrink-0 place-items-center rounded-lg text-emerald-700 hover:bg-emerald-100 hover:text-emerald-950 focus-visible:outline-2 focus-visible:outline-emerald-700">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </li>
  </ul>
  <div id="tal-dis-empty" hidden class="mt-4 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-dashed border-slate-300 p-4 text-sm text-slate-600">
    <p>You are all caught up.</p>
    <button type="button" id="tal-dis-restore" class="min-h-10 rounded-lg px-3 font-semibold text-sky-800 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Show notices again</button>
  </div>
</section>

Alerts with Retry and Later actions

  • aria-disabled:
  • motion-safe:animate-spin
  • role="status"
  • sm:flex-row

Use the buttons inside the alert.

The failed upload sits inside a role="status" wrapper that exists from the start, so swapping it for the success box is announced. Retry marks itself with aria-disabled, which the aria-disabled: variant dims, and shows a spinner that only turns under motion-safe:. After each action focus goes to the new link or the heading, because the button the user pressed disappears.

View code
<section aria-labelledby="tal-act-title" class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-200">
  <h3 id="tal-act-title" tabindex="-1" class="rounded-sm font-semibold text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-900">Media library</h3>
  <div role="status" class="mt-4">
    <div id="tal-act-fail" class="flex gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900">
      <svg class="size-5 shrink-0 text-red-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
      <div class="min-w-0 flex-1">
        <p class="font-semibold"><span class="sr-only">Error: </span>Upload failed</p>
        <p class="mt-1 text-red-800">site-photos.zip (84 MB) stopped at 62% because the connection dropped.</p>
        <div class="mt-3 flex flex-wrap gap-2">
          <button type="button" id="tal-act-retry" class="inline-flex min-h-10 items-center gap-2 rounded-lg bg-red-700 px-3.5 font-semibold text-white hover:bg-red-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-700 aria-disabled:cursor-wait aria-disabled:bg-red-800">
            <svg id="tal-act-spin" class="hidden size-4 motion-safe:animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-opacity=".3" stroke-width="3"/><path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>
            <span id="tal-act-label">Retry upload</span>
          </button>
          <a href="#" class="inline-flex min-h-10 items-center rounded-lg px-3.5 font-semibold text-red-800 hover:bg-red-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-700">Upload a smaller file</a>
        </div>
      </div>
    </div>
    <div id="tal-act-ok" hidden class="flex gap-3 rounded-xl border border-green-200 bg-green-50 p-4 text-sm text-green-900">
      <svg class="size-5 shrink-0 text-green-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
      <div class="min-w-0 flex-1">
        <p class="font-semibold"><span class="sr-only">Success: </span>Upload complete</p>
        <p class="mt-1 text-green-800">site-photos.zip is in the library with 312 images.</p>
        <a id="tal-act-view" href="#" class="mt-2 inline-block rounded-sm font-semibold text-green-800 underline underline-offset-4 hover:text-green-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-700">Open the album</a>
      </div>
    </div>
  </div>
  <div role="status" class="mt-3">
    <div id="tal-act-upd" class="flex flex-col gap-3 rounded-xl bg-zinc-900 p-4 text-sm text-zinc-100 sm:flex-row sm:items-center">
      <svg class="hidden size-5 shrink-0 text-violet-300 sm:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3M18 3v4h-4M6 21v-4h4"/></svg>
      <p id="tal-act-upd-text" class="min-w-0 flex-1">A new version of the editor is ready. Reload to get the faster crop tool.</p>
      <div id="tal-act-upd-btns" class="flex gap-2">
        <button type="button" id="tal-act-reload" class="min-h-10 rounded-lg bg-white px-3.5 font-semibold text-zinc-900 hover:bg-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">Reload</button>
        <button type="button" id="tal-act-later" class="min-h-10 rounded-lg px-3.5 font-semibold text-zinc-300 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">Later</button>
      </div>
    </div>
  </div>
</section>

Build error with collapsible details

  • <details>
  • group-open:
  • list-none
  • wrap-break-word

Open the error list.

The summary line stays short and the compiler output waits inside a native details element, so the toggle needs no JavaScript and works with Enter and Space. group-open: swaps the Show and Hide labels and turns the chevron, list-none plus [&::-webkit-details-marker]:hidden removes the default triangle, and wrap-break-word keeps long file paths inside the frame on phones.

View code
<div class="w-full max-w-xl rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-950 sm:p-5">
  <div class="flex gap-3">
    <svg class="size-5 shrink-0 text-rose-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
    <div class="min-w-0 flex-1">
      <p class="font-semibold"><span class="sr-only">Error: </span>Build failed on main</p>
      <p class="mt-1 text-rose-900">3 type errors in 2 files. The deploy from 13:58 is still live, so visitors see no change.</p>
      <details id="tal-det" class="group mt-2">
        <summary class="inline-flex min-h-10 cursor-pointer list-none items-center gap-1.5 rounded-lg font-semibold text-rose-800 hover:text-rose-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-700 [&::-webkit-details-marker]:hidden">
          <span class="group-open:hidden">Show the 3 errors</span><span class="hidden group-open:inline">Hide the errors</span>
          <svg class="size-4 group-open:rotate-180 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6"/></svg>
        </summary>
        <ol class="mt-2 space-y-3 rounded-lg bg-zinc-950 p-3.5 font-mono text-xs leading-5 text-zinc-100 wrap-break-word">
          <li><span class="text-rose-300">src/cart/total.ts:42:18</span><br>TS2322: Type 'string' is not assignable to type 'number'.</li>
          <li><span class="text-rose-300">src/cart/total.ts:57:9</span><br>TS2339: Property 'discount' does not exist on type 'Cart'.</li>
          <li><span class="text-rose-300">src/checkout/address.tsx:12:3</span><br>TS2741: Property 'postcode' is missing in type '{ city: string; }'.</li>
        </ol>
        <a href="#" class="mt-3 inline-block rounded-sm font-semibold text-rose-800 underline underline-offset-4 hover:text-rose-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-700">Open the full build log</a>
      </details>
    </div>
  </div>
</div>

Live regions and form errors

How messages reach screen readers: live regions that exist empty at load and get filled later, field-level errors wired with aria-invalid and aria-describedby, and a page-level error summary that takes focus and links to each field.

Polite and assertive live regions

  • role="status"
  • role="alert"
  • <template>
  • not-empty:

Press Save draft, then Publish.

Both regions are in the markup from the start and empty, which is what makes screen readers watch them; the JS clones a template, fills in the text with textContent, empties the region and adds the new message 100 ms later, so even a repeated message counts as a change. not-empty:mt-4 adds the spacing only when a message is inside, and an empty region has no padding, so it takes no room. The dark panel mirrors what each region would say and at which politeness level.

View code
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
  <h3 class="font-semibold text-slate-900">Spring menu launch</h3>
  <p class="mt-1 text-sm text-slate-600">Blog post, last edited 4 minutes ago</p>
  <div class="mt-4 flex flex-wrap gap-2">
    <button type="button" id="tal-lr-save" class="min-h-11 rounded-lg border border-slate-300 bg-white px-4 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600">Save draft</button>
    <button type="button" id="tal-lr-pub" class="min-h-11 rounded-lg bg-violet-700 px-4 text-sm font-semibold text-white hover:bg-violet-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">Publish</button>
    <button type="button" id="tal-lr-clear" class="min-h-11 rounded-lg px-3 text-sm font-semibold text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600">Clear</button>
  </div>
  <div id="tal-lr-polite" role="status" class="not-empty:mt-4"></div>
  <div id="tal-lr-urgent" role="alert" class="not-empty:mt-3"></div>
  <div class="mt-5 rounded-xl bg-slate-950 p-4 text-slate-100">
    <p class="text-xs font-semibold text-slate-300">What a screen reader would say</p>
    <ol id="tal-lr-log" class="mt-2 space-y-1.5 font-mono text-xs leading-5"><li class="text-slate-400">Nothing announced yet.</li></ol>
  </div>
  <template id="tal-lr-ok">
    <div class="flex gap-3 rounded-xl border border-green-200 bg-green-50 p-3.5 text-sm text-green-900">
      <svg class="size-5 shrink-0 text-green-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
      <p><span class="font-semibold">Draft saved.</span> <span class="tal-lr-text"></span></p>
    </div>
  </template>
  <template id="tal-lr-err">
    <div class="flex gap-3 rounded-xl border border-red-200 bg-red-50 p-3.5 text-sm text-red-900">
      <svg class="size-5 shrink-0 text-red-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
      <p><span class="font-semibold">Publish failed.</span> <span class="tal-lr-text"></span></p>
    </div>
  </template>
</div>

Inline field errors

  • aria-[invalid=true]:
  • aria-describedby
  • novalidate
  • accent-*

Submit the empty form, then fix the fields.

Each input points at its hint and error text with aria-describedby, and the JS sets aria-invalid, which the arbitrary aria-[invalid=true]: variant turns into a red border and tint. Fields are checked on blur once they have a value (skipped when focus moves to the submit button, so a new message cannot push the button away mid-click) and re-checked on every keystroke while wrong. On submit focus goes to the first invalid field, and the success line is an empty role="status" filled at the end.

View code
<form id="tal-fe" novalidate class="w-full max-w-sm rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
  <h3 class="text-lg font-semibold text-stone-900">Create your account</h3>
  <div class="mt-5">
    <label for="tal-fe-email" class="block text-sm font-medium text-stone-800">Work email</label>
    <input id="tal-fe-email" type="email" autocomplete="email" required aria-describedby="tal-fe-email-err" placeholder="you@example.com" class="mt-1.5 block min-h-11 w-full rounded-lg border border-stone-300 bg-white px-3 text-sm text-stone-900 placeholder:text-stone-400 focus:border-stone-900 focus:outline-3 focus:outline-stone-900/15 aria-[invalid=true]:border-red-600 aria-[invalid=true]:bg-red-50/50 aria-[invalid=true]:focus:outline-red-600/20">
    <p id="tal-fe-email-err" hidden class="mt-1.5 flex gap-1.5 text-sm text-red-700"><svg class="mt-0.5 size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7.5v5.5m0 3.5v.01"/></svg><span class="tal-fe-msg"></span></p>
  </div>
  <div class="mt-4">
    <label for="tal-fe-pass" class="block text-sm font-medium text-stone-800">Password</label>
    <input id="tal-fe-pass" type="password" autocomplete="new-password" required aria-describedby="tal-fe-pass-hint tal-fe-pass-err" class="mt-1.5 block min-h-11 w-full rounded-lg border border-stone-300 bg-white px-3 text-sm text-stone-900 focus:border-stone-900 focus:outline-3 focus:outline-stone-900/15 aria-[invalid=true]:border-red-600 aria-[invalid=true]:bg-red-50/50 aria-[invalid=true]:focus:outline-red-600/20">
    <p id="tal-fe-pass-hint" class="mt-1.5 text-sm text-stone-500">At least 12 characters.</p>
    <p id="tal-fe-pass-err" hidden class="mt-1 flex gap-1.5 text-sm text-red-700"><svg class="mt-0.5 size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7.5v5.5m0 3.5v.01"/></svg><span class="tal-fe-msg"></span></p>
  </div>
  <div class="mt-4">
    <label class="flex items-start gap-2.5 text-sm text-stone-700">
      <input id="tal-fe-terms" type="checkbox" aria-describedby="tal-fe-terms-err" class="mt-0.5 size-4 shrink-0 accent-stone-900">
      <span>I agree to the terms of service and the privacy policy.</span>
    </label>
    <p id="tal-fe-terms-err" hidden class="mt-1.5 flex gap-1.5 text-sm text-red-700"><svg class="mt-0.5 size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="M12 7.5v5.5m0 3.5v.01"/></svg><span class="tal-fe-msg"></span></p>
  </div>
  <button type="submit" class="mt-6 min-h-11 w-full rounded-lg bg-stone-900 px-4 text-sm font-semibold text-white hover:bg-stone-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">Create account</button>
  <p id="tal-fe-done" role="status" class="text-sm font-medium text-green-700 not-empty:mt-3"></p>
</form>

Error summary that takes focus

  • tabindex="-1"
  • has-[[aria-invalid=true]]:
  • focus:outline-amber-400
  • href="#id"

Press Continue, then use a link in the summary.

On submit the summary box appears above the form, gets focus through tabindex="-1" and lists one link per problem, so a screen reader hears the count and the list at once. Each link points at the field id and the JS focuses that field, which also scrolls it into view. Field wrappers use has-[[aria-invalid=true]]: to draw a red left bar, the GOV.UK pattern, without any extra class from the script.

View code
<form id="tal-es" novalidate class="w-full max-w-md rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200">
  <div id="tal-es-sum" tabindex="-1" aria-labelledby="tal-es-sum-title" hidden class="mb-6 rounded-xl border-2 border-red-700 p-4 focus:outline-3 focus:outline-offset-2 focus:outline-amber-400">
    <h3 id="tal-es-sum-title" class="flex items-center gap-2 font-semibold text-red-800">
      <svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
      <span id="tal-es-sum-text">There is a problem</span>
    </h3>
    <ul id="tal-es-list" class="mt-3 grid gap-1.5 ps-7 text-sm"></ul>
  </div>
  <h3 class="text-lg font-semibold text-slate-900">Shipping address</h3>
  <div class="mt-5 grid gap-4">
    <div class="has-[[aria-invalid=true]]:border-s-4 has-[[aria-invalid=true]]:border-red-700 has-[[aria-invalid=true]]:ps-3">
      <label for="tal-es-name" class="block text-sm font-medium text-slate-800">Full name</label>
      <p id="tal-es-name-err" hidden class="mt-1 text-sm font-semibold text-red-700"><span class="sr-only">Error: </span><span class="tal-es-msg"></span></p>
      <input id="tal-es-name" type="text" autocomplete="name" aria-describedby="tal-es-name-err" class="mt-1.5 block min-h-11 w-full rounded-lg border border-slate-400 px-3 text-sm text-slate-900 focus:outline-3 focus:outline-offset-0 focus:outline-amber-400 aria-[invalid=true]:border-red-700">
    </div>
    <div class="has-[[aria-invalid=true]]:border-s-4 has-[[aria-invalid=true]]:border-red-700 has-[[aria-invalid=true]]:ps-3">
      <label for="tal-es-street" class="block text-sm font-medium text-slate-800">Street and house number</label>
      <p id="tal-es-street-err" hidden class="mt-1 text-sm font-semibold text-red-700"><span class="sr-only">Error: </span><span class="tal-es-msg"></span></p>
      <input id="tal-es-street" type="text" autocomplete="street-address" aria-describedby="tal-es-street-err" class="mt-1.5 block min-h-11 w-full rounded-lg border border-slate-400 px-3 text-sm text-slate-900 focus:outline-3 focus:outline-offset-0 focus:outline-amber-400 aria-[invalid=true]:border-red-700">
    </div>
    <div class="has-[[aria-invalid=true]]:border-s-4 has-[[aria-invalid=true]]:border-red-700 has-[[aria-invalid=true]]:ps-3">
      <label for="tal-es-zip" class="block text-sm font-medium text-slate-800">ZIP code</label>
      <p id="tal-es-zip-err" hidden class="mt-1 text-sm font-semibold text-red-700"><span class="sr-only">Error: </span><span class="tal-es-msg"></span></p>
      <input id="tal-es-zip" type="text" inputmode="numeric" autocomplete="postal-code" aria-describedby="tal-es-zip-err" class="mt-1.5 block min-h-11 w-32 rounded-lg border border-slate-400 px-3 text-sm text-slate-900 focus:outline-3 focus:outline-offset-0 focus:outline-amber-400 aria-[invalid=true]:border-red-700">
    </div>
  </div>
  <button type="submit" class="mt-6 min-h-11 rounded-lg bg-emerald-800 px-5 text-sm font-semibold text-white hover:bg-emerald-900 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-amber-400">Continue to payment</button>
  <p id="tal-es-ok" role="status" class="text-sm font-medium text-emerald-800 not-empty:mt-3"></p>
</form>

Banners, bars and toasts

Page-level messages: a flush billing banner, an announcement bar with manual controls, a system status bar driven by data-* variants, a non-modal cookie notice and two toast patterns that pause on hover and focus.

Flush trial banner with dismiss

  • role="region"
  • whitespace-nowrap
  • max-w-5xl
  • md:grid-cols-4

Close the banner.

A full-width banner above the app header, labelled as a region so screen reader users can find or skip it. Amber-300 with amber-950 text keeps contrast high without red alarm, and the link uses whitespace-nowrap so Add billing never breaks across lines. Dismissing hides the banner and moves focus to the page heading, which has tabindex="-1" for exactly that job.

View code
<div class="bg-slate-50">
  <div id="tal-ban" role="region" aria-label="Trial notice" class="bg-amber-300 text-amber-950">
    <div class="mx-auto flex max-w-5xl items-start gap-3 px-4 py-2.5 sm:items-center sm:px-6">
      <svg class="mt-0.5 size-5 shrink-0 sm:mt-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="M12 7v5l3 2"/></svg>
      <p class="min-w-0 flex-1 text-sm"><span class="font-semibold">Your trial ends in 3 days.</span> Add a payment method to keep your 4 projects online. <a href="#" class="whitespace-nowrap rounded-sm font-semibold underline underline-offset-4 hover:no-underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-950">Add billing</a></p>
      <button type="button" id="tal-ban-close" aria-label="Dismiss trial notice" class="-my-1 grid size-9 shrink-0 place-items-center rounded-md hover:bg-amber-400 focus-visible:outline-2 focus-visible:outline-amber-950">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </div>
  </div>
  <header class="border-b border-slate-200 bg-white">
    <div class="mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 py-3 sm:px-6">
      <a href="#" class="flex items-center gap-2 rounded-md font-semibold text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-900"><span class="grid size-8 place-items-center rounded-lg bg-slate-900 text-sm font-bold text-white" aria-hidden="true">P</span>Parcelwise</a>
      <nav aria-label="Main" class="hidden gap-6 text-sm font-medium text-slate-600 md:flex"><a href="#" aria-current="page" class="text-slate-900">Projects</a><a href="#" class="hover:text-slate-900">Deploys</a><a href="#" class="hover:text-slate-900">Domains</a><a href="#" class="hover:text-slate-900">Settings</a></nav>
      <svg class="size-9 shrink-0" viewBox="0 0 36 36" role="img" aria-label="Signed in as Rosa Lind"><circle cx="18" cy="18" r="18" fill="#e0e7ff"/><text x="18" y="23" text-anchor="middle" font-size="13" font-weight="600" fill="#3730a3" font-family="system-ui, sans-serif">RL</text></svg>
    </div>
  </header>
  <main class="mx-auto max-w-5xl px-4 py-6 sm:px-6">
    <h2 id="tal-ban-h" tabindex="-1" class="rounded-sm text-xl font-semibold text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-slate-900">Projects</h2>
    <ul class="mt-4 grid gap-3 sm:grid-cols-2 md:grid-cols-4">
      <li class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="font-medium text-slate-900">harbor-docs</p><p class="mt-1 text-sm text-slate-500">Deployed 2 hours ago</p></li>
      <li class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="font-medium text-slate-900">shop-frontend</p><p class="mt-1 text-sm text-slate-500">Deployed yesterday</p></li>
      <li class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="font-medium text-slate-900">status-page</p><p class="mt-1 text-sm text-slate-500">Deployed 3 days ago</p></li>
      <li class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="font-medium text-slate-900">brand-kit</p><p class="mt-1 text-sm text-slate-500">Deployed last week</p></li>
    </ul>
  </main>
</div>

Announcement bar with manual controls

  • aria-live="polite"
  • bg-stone-950
  • tabular-nums
  • hidden sm:inline

Use the arrows to switch messages.

A thin stone-950 bar that cycles three messages with previous and next buttons instead of a timer, because content that moves on its own for more than five seconds needs a pause control under WCAG 2.2.2. The message paragraph is aria-live="polite", so the new text is read after each press, and the 1 / 3 counter appears from the sm breakpoint up.

View code
<div class="bg-white">
  <section aria-label="Announcements" class="bg-stone-950 text-stone-100">
    <div class="mx-auto flex max-w-5xl items-center gap-2 px-2 py-1.5 sm:px-4">
      <button type="button" id="tal-ann-prev" aria-label="Previous announcement" class="grid size-9 shrink-0 place-items-center rounded-md text-stone-300 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m15 6-6 6 6 6"/></svg>
      </button>
      <p aria-live="polite" class="min-w-0 flex-1 text-center text-sm"><span id="tal-ann-msg">Free shipping on orders over $60 until November 30.</span> <a href="#" class="whitespace-nowrap rounded-sm font-semibold text-amber-300 underline underline-offset-4 hover:text-amber-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-300">See details</a></p>
      <span id="tal-ann-pos" class="hidden text-xs text-stone-400 tabular-nums sm:inline">1 / 3</span>
      <button type="button" id="tal-ann-next" aria-label="Next announcement" class="grid size-9 shrink-0 place-items-center rounded-md text-stone-300 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m9 6 6 6-6 6"/></svg>
      </button>
    </div>
  </section>
  <header class="border-b border-stone-200">
    <div class="mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 py-4 sm:px-6">
      <a href="#" class="rounded-sm font-serif text-xl font-semibold text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">Fernhill Supply</a>
      <nav aria-label="Shop" class="hidden gap-6 text-sm text-stone-700 md:flex"><a href="#" class="hover:text-stone-950">Kitchen</a><a href="#" class="hover:text-stone-950">Garden</a><a href="#" class="hover:text-stone-950">Workshop</a><a href="#" class="hover:text-stone-950">Journal</a></nav>
      <a href="#" class="inline-flex min-h-10 items-center gap-2 rounded-full border border-stone-300 px-4 text-sm font-medium text-stone-800 hover:bg-stone-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">Cart <span class="rounded-full bg-stone-900 px-1.5 text-xs text-white tabular-nums">2</span></a>
    </div>
  </header>
  <div class="mx-auto max-w-5xl px-4 py-10 sm:px-6">
    <h2 class="max-w-xl font-serif text-3xl text-stone-900 text-balance">Tools that last longer than the season</h2>
    <p class="mt-3 max-w-lg text-stone-600">Hand-forged trowels, linen aprons and cast iron, picked by people who use them every weekend.</p>
  </div>
</div>

System status bar with data-state

  • data-[state=outage]:
  • group-data-[state=ok]:block
  • has-checked:
  • role="status"

Pick a state to simulate.

The bar carries one data-state attribute, and data-[state=...]: variants pick its color while group-data-[state=...]: shows the matching icon and message, so the JS only writes the attribute. Because the bar is a role="status" region, the newly shown message is announced. The maintenance text uses a time element with a machine-readable datetime.

View code
<div class="bg-zinc-50">
  <div id="tal-sys" data-state="maintenance" role="status" class="group bg-emerald-700 text-white data-[state=degraded]:bg-amber-300 data-[state=degraded]:text-amber-950 data-[state=maintenance]:bg-sky-800 data-[state=outage]:bg-red-700">
    <div class="mx-auto flex max-w-5xl items-start gap-3 px-4 py-3 sm:items-center sm:px-6">
      <svg class="hidden size-5 shrink-0 group-data-[state=ok]:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5 2.5 2.5 4.5-5"/></svg>
      <svg class="hidden size-5 shrink-0 group-data-[state=maintenance]:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M14.7 6.3a4 4 0 0 0-5.4 5.4L4 17v3h3l5.3-5.3a4 4 0 0 0 5.4-5.4l-2.6 2.6-2.4-.6-.6-2.4Z"/></svg>
      <svg class="hidden size-5 shrink-0 group-data-[state=degraded]:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 3.5v.01M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20.3h15.4a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>
      <svg class="hidden size-5 shrink-0 group-data-[state=outage]:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path stroke-linecap="round" d="m9 9 6 6m0-6-6 6"/></svg>
      <div class="min-w-0 flex-1 text-sm">
        <p class="hidden group-data-[state=ok]:block"><span class="font-semibold">All systems operational.</span> No incidents in the last 7 days.</p>
        <p class="hidden group-data-[state=maintenance]:block"><span class="font-semibold">Scheduled maintenance.</span> The API is read-only on <time datetime="2026-11-02T02:00Z">Sunday, November 2, 02:00 to 03:00 UTC</time>.</p>
        <p class="hidden group-data-[state=degraded]:block"><span class="font-semibold">Degraded performance.</span> Webhooks in eu-central are delayed by up to 10 minutes.</p>
        <p class="hidden group-data-[state=outage]:block"><span class="font-semibold">Major outage.</span> The dashboard and API are down. Updates every 15 minutes.</p>
      </div>
      <a href="#" class="whitespace-nowrap rounded-sm text-sm font-semibold underline underline-offset-4 hover:no-underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current">Status page</a>
    </div>
  </div>
  <div class="mx-auto max-w-5xl px-4 py-6 sm:px-6">
    <div class="flex flex-wrap items-end justify-between gap-4">
      <div>
        <h2 class="text-xl font-semibold text-zinc-900">Corvid Cloud</h2>
        <p class="mt-1 text-sm text-zinc-600">99.94% uptime over the last 30 days</p>
      </div>
      <fieldset>
        <legend class="text-sm font-medium text-zinc-700">Simulate a state</legend>
        <div class="mt-2 flex flex-wrap gap-1.5">
          <label id="tal-sys-l-ok" class="cursor-pointer rounded-full border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-700 hover:border-zinc-400 has-checked:border-zinc-900 has-checked:bg-zinc-900 has-checked:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="radio" name="tal-sys" value="ok" class="tal-sys-opt sr-only">Operational</label>
          <label id="tal-sys-l-maint" class="cursor-pointer rounded-full border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-700 hover:border-zinc-400 has-checked:border-zinc-900 has-checked:bg-zinc-900 has-checked:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="radio" name="tal-sys" value="maintenance" class="tal-sys-opt sr-only" checked>Maintenance</label>
          <label id="tal-sys-l-deg" class="cursor-pointer rounded-full border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-700 hover:border-zinc-400 has-checked:border-zinc-900 has-checked:bg-zinc-900 has-checked:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="radio" name="tal-sys" value="degraded" class="tal-sys-opt sr-only">Degraded</label>
          <label id="tal-sys-l-out" class="cursor-pointer rounded-full border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-700 hover:border-zinc-400 has-checked:border-zinc-900 has-checked:bg-zinc-900 has-checked:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-900"><input type="radio" name="tal-sys" value="outage" class="tal-sys-opt sr-only">Outage</label>
        </div>
      </fieldset>
    </div>
    <div class="mt-5 flex h-9 gap-0.5" aria-hidden="true">
      <span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-amber-400"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span>
      <span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-red-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span>
      <span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span><span class="flex-1 rounded-sm bg-emerald-500"></span>
    </div>
    <div class="mt-2 flex justify-between text-xs text-zinc-500"><span>30 days ago</span><span>Today</span></div>
  </div>
</div>

Toast stack that pauses on hover

  • motion-safe:starting:opacity-0
  • data-[kind=err]:
  • group-data-[kind=ok]:block
  • pointer-events-none

Fire a few toasts, then hover the stack.

Toasts land in an aria-live list in the bottom-right corner and enter with starting:opacity-0 and starting:translate-y-3 inside motion-safe:, which Tailwind compiles to @starting-style. Success and info close after 5 seconds, the timers pause while the pointer or focus is in the stack, and errors stay until closed, which keeps the timing within WCAG 2.2.1. data-kind on each toast picks its icon and ring through data-* and group-data-* variants.

View code
<div class="relative min-h-[520px] w-full max-w-2xl overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200 sm:min-h-[420px]">
  <div class="flex items-center justify-between gap-3 border-b border-slate-200 px-5 py-3">
    <h3 class="font-semibold text-slate-900">Team settings</h3>
    <span class="text-sm text-slate-500">Harbor Studio</span>
  </div>
  <div class="p-5">
    <p class="max-w-md text-sm text-slate-600">Success and info toasts close after 5 seconds. Hover or focus the stack to pause them. Errors stay until you close them.</p>
    <div class="mt-4 flex flex-wrap gap-2">
      <button type="button" data-toast="save" class="tal-ts-btn min-h-11 rounded-lg bg-indigo-600 px-4 text-sm font-semibold text-white hover:bg-indigo-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Save changes</button>
      <button type="button" data-toast="invite" class="tal-ts-btn min-h-11 rounded-lg border border-slate-300 px-4 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Send invite</button>
      <button type="button" data-toast="sync" class="tal-ts-btn min-h-11 rounded-lg border border-slate-300 px-4 text-sm font-semibold text-red-700 hover:bg-red-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sync calendar</button>
    </div>
  </div>
  <ol id="tal-ts-list" aria-live="polite" aria-label="Notifications" class="pointer-events-none absolute inset-x-3 bottom-3 flex flex-col gap-2 sm:inset-x-auto sm:right-4 sm:bottom-4 sm:w-80"></ol>
  <template id="tal-ts-tpl">
    <li class="group pointer-events-auto flex gap-3 rounded-xl bg-white p-3.5 text-sm shadow-lg ring-1 ring-slate-900/10 data-[kind=err]:ring-red-300 motion-safe:transition-[opacity,translate] motion-safe:duration-300 motion-safe:starting:translate-y-3 motion-safe:starting:opacity-0">
      <span class="grid size-8 shrink-0 place-items-center rounded-full bg-emerald-100 text-emerald-700 group-data-[kind=err]:bg-red-100 group-data-[kind=err]:text-red-700 group-data-[kind=info]:bg-sky-100 group-data-[kind=info]:text-sky-700">
        <svg class="hidden size-4 group-data-[kind=ok]:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m5 12.5 4.5 4.5L19 7.5"/></svg>
        <svg class="hidden size-4 group-data-[kind=info]:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16v12H4zM4 7l8 6 8-6"/></svg>
        <svg class="hidden size-4 group-data-[kind=err]:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"><path stroke-linecap="round" d="M12 6v7m0 4.5v.01"/></svg>
      </span>
      <div class="min-w-0 flex-1">
        <p class="tal-ts-title font-semibold text-slate-900"></p>
        <p class="tal-ts-text mt-0.5 text-slate-600"></p>
      </div>
      <button type="button" aria-label="Dismiss notification" class="tal-ts-close -m-1 grid size-8 shrink-0 place-items-center rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-indigo-600">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </li>
  </template>
</div>

Undo toast with countdown bar

  • motion-safe:starting:translate-y-3
  • divide-y
  • truncate
  • aria-live

Archive a conversation, then hover the toast.

Archiving a conversation hides the row and shows a dark toast with Undo and a teal bar that drains over 6 seconds. The countdown is a chained setTimeout that subtracts real elapsed time, so hovering or focusing the toast freezes both the timer and the bar. Undo brings the row back and returns focus to its Archive button; after archiving, focus moves to the next row so the keyboard user stays in the list.

View code
<div class="relative min-h-[400px] w-full max-w-lg overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-neutral-200">
  <div class="flex items-center justify-between gap-3 border-b border-neutral-200 px-5 py-3">
    <h3 id="tal-tp-title" tabindex="-1" class="rounded-sm font-semibold text-neutral-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Inbox</h3>
    <span id="tal-tp-count" class="text-sm text-neutral-500">3 conversations</span>
  </div>
  <ul class="divide-y divide-neutral-100">
    <li class="tal-tp-row flex items-center gap-3 px-5 py-3.5" data-name="Quote for the Lisbon offsite">
      <span class="grid size-9 shrink-0 place-items-center rounded-full bg-orange-100 text-sm font-semibold text-orange-800" aria-hidden="true">MO</span>
      <div class="min-w-0 flex-1"><p class="truncate text-sm font-semibold text-neutral-900">Mira Okafor</p><p class="truncate text-sm text-neutral-600">Quote for the Lisbon offsite</p></div>
      <button type="button" aria-label="Archive: Quote for the Lisbon offsite" class="tal-tp-archive grid size-10 shrink-0 place-items-center rounded-lg text-neutral-500 hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline-2 focus-visible:outline-teal-700">
        <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16v4H4zM5.5 10v9h13v-9M10 14h4"/></svg>
      </button>
    </li>
    <li class="tal-tp-row flex items-center gap-3 px-5 py-3.5" data-name="Invoice 1187 is ready">
      <span class="grid size-9 shrink-0 place-items-center rounded-full bg-teal-100 text-sm font-semibold text-teal-800" aria-hidden="true">DR</span>
      <div class="min-w-0 flex-1"><p class="truncate text-sm font-semibold text-neutral-900">Dev Raman</p><p class="truncate text-sm text-neutral-600">Invoice 1187 is ready</p></div>
      <button type="button" aria-label="Archive: Invoice 1187 is ready" class="tal-tp-archive grid size-10 shrink-0 place-items-center rounded-lg text-neutral-500 hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline-2 focus-visible:outline-teal-700">
        <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16v4H4zM5.5 10v9h13v-9M10 14h4"/></svg>
      </button>
    </li>
    <li class="tal-tp-row flex items-center gap-3 px-5 py-3.5" data-name="Notes from the Tuesday review">
      <span class="grid size-9 shrink-0 place-items-center rounded-full bg-violet-100 text-sm font-semibold text-violet-800" aria-hidden="true">LF</span>
      <div class="min-w-0 flex-1"><p class="truncate text-sm font-semibold text-neutral-900">Lena Fischer</p><p class="truncate text-sm text-neutral-600">Notes from the Tuesday review</p></div>
      <button type="button" aria-label="Archive: Notes from the Tuesday review" class="tal-tp-archive grid size-10 shrink-0 place-items-center rounded-lg text-neutral-500 hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline-2 focus-visible:outline-teal-700">
        <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16v4H4zM5.5 10v9h13v-9M10 14h4"/></svg>
      </button>
    </li>
  </ul>
  <p id="tal-tp-empty" hidden class="px-5 py-10 text-center text-sm text-neutral-500">Inbox zero. Nothing left to sort.</p>
  <div id="tal-tp-region" aria-live="polite" class="pointer-events-none absolute inset-x-3 bottom-3 sm:inset-x-auto sm:right-4 sm:bottom-4 sm:w-80"></div>
  <template id="tal-tp-tpl">
    <div class="tal-tp-toast pointer-events-auto overflow-hidden rounded-xl bg-neutral-900 text-sm text-white shadow-xl motion-safe:transition-[opacity,translate] motion-safe:duration-300 motion-safe:starting:translate-y-3 motion-safe:starting:opacity-0">
      <div class="flex items-center gap-2 py-2 ps-4 pe-2">
        <p class="min-w-0 flex-1 truncate"><span class="font-semibold">Archived:</span> <span class="tal-tp-text"></span></p>
        <button type="button" class="tal-tp-undo min-h-10 shrink-0 rounded-lg px-3 font-semibold text-teal-300 hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-teal-300">Undo</button>
        <button type="button" aria-label="Dismiss" class="tal-tp-close grid size-10 shrink-0 place-items-center rounded-lg text-neutral-400 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white">
          <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>
      <div class="h-1 bg-white/10" aria-hidden="true"><div class="tal-tp-bar h-full w-full bg-teal-400"></div></div>
    </div>
  </template>
</div>

Does Tailwind CSS include an alert component?

No. Tailwind works through single-purpose utilities (the Tailwind basics take five minutes to read), so you assemble the alert yourself. The Tailwind v1 documentation already described alerts as something easy to build from existing utilities.

If you’d rather not assemble it, finished versions exist. Tailwind Plus, the paid collection from Tailwind Labs, lists them under Application UI, Feedback. daisyUI and Flowbite add alert classes or markup of their own. The plain route is still there too: a div, a role attribute and a handful of classes.

Bootstrap goes the other way. Its documentation lists eight contextual classes, from alert-primary to alert-dark, so one class name gives you a finished box (Bootstrap 5 documentation).

Tailwind gives up that convenience and hands you control over every spacing and color value in return. Weighing the two? This comparison of Tailwind and Bootstrap goes into the wider trade-off.

How to build a Tailwind alert with utility classes

Start with a flex container, give it padding, a rounded border and a tinted background, then put the icon and the message inside. Most of the work happens in flex, gap-3, rounded-lg, border, p-4 and text-sm.

  1. Create a div and give it the utilities above.
  2. Pick one color family and match background, border and text, for example bg-blue-50 border-blue-200 text-blue-800. A pale fill with dark text from the same family stays readable.
  3. Add the icon as an inline SVG with size-5 shrink-0, so wrapping text can’t squash it.
  4. Write the title in font-medium and put the description underneath.
  5. Set role="alert" for errors and role="status" for routine information.
  6. Paste the markup into the Tailwind Play browser editor and test it before it goes anywhere near a project.
<div role="status" class="flex gap-3 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800">

  <svg class="size-5 shrink-0" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
    <circle cx="10" cy="10" r="8" />
    <path d="M10 9v5M10 6.5v.01" stroke-linecap="round" />
  </svg>
  <div>
    <p class="font-medium">Plan renews soon</p>
    <p>Your subscription renews in 3 days.</p>
  </div>
</div>

Adding an icon and a title

Heroicons is the natural pick. Tailwind Labs maintains it, and the SVGs take utility classes directly. It comes in outline and solid at 24px, mini at 20px and micro at 16px.

The micro set arrived in version 2.1.0 on December 18, 2023, according to the Heroicons changelog. The 20px mini set matches size-5 and sits well beside text-sm copy.

I’d take an inline SVG straight into the HTML over an icon font here. The icon picks up the text color through currentColor, so it follows the variant with no extra classes.

Decorative icons get aria-hidden="true", which is what Flowbite’s own alert markup does.

Short messages read fine with a bold lead-in on the same line, which is how Flowbite writes its examples. Longer ones need a stacked title and description, like the sample above.

Tailwind alert variants for success, error, warning and info

The structure never changes. What differs between the four is the color family and the icon, plus the ARIA role.

VariantColor classesRoleHeroicons icon
Successbg-green-50 border-green-200 text-green-800statuscheck-circle
Errorbg-red-50 border-red-200 text-red-800alertx-circle
Warningbg-amber-50 border-amber-200 text-amber-800status, or alert if urgentexclamation-triangle
Infobg-blue-50 border-blue-200 text-blue-800statusinformation-circle

Color is only one axis. The same four states also come in several visual treatments.

The soft version is the tinted fill from the table (alert-soft in daisyUI). Outline drops the fill and keeps the border (alert-outline in daisyUI, and an outlined variant in Material Tailwind). daisyUI also has alert-dash for a dashed edge, and Material Tailwind builds gradients from from- and to- utilities. An accent border puts a thick edge in the variant color, border-l-4 for the left or border-s-4 for layouts that flip in RTL.

daisyUI covers the whole grid with 4 color classes (alert-info, alert-success, alert-warning, alert-error) and 3 style modifiers, per its documentation.

Libraries disagree on what to call the red one. daisyUI says error, Flowbite and Bootstrap say danger, and shadcn/ui says destructive. Material Tailwind skips semantic names and takes a color, so its red alert is color="red".

Pick one name for your own components and map the library names onto it.

Copy accent-border snippets with care. The Flowbite Svelte docs describe the accent as a left edge, yet the example code uses border-t-4, which draws it on top.

How to make a Tailwind alert dismissible

A dismissible alert needs a close button and something that hides the alert when the button is clicked. That something can be a few lines of JavaScript, an Alpine.js x-show binding, a React state flag or Flowbite’s data-dismiss-target attribute.

Whichever route you pick, the button should have type="button" so it never submits a surrounding form. Add aria-label="Close", since the visible content is only a symbol, and give it a visible focus ring such as focus:ring-2.

Style it like any other Tailwind button, with small padding, rounded corners, ms-auto to push it to the end of the flex row and a hover background in the variant color.

Plain JavaScript and Flowbite data attributes

Honestly, a few lines of JavaScript cover every case here, and the first one is a single click handler that adds hidden to the alert.

<div id="alert-1" role="alert" class="flex rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800">
  <p>Payment failed. Update your card and try again.</p>
  <button type="button" aria-label="Close" class="ms-auto rounded-lg p-1.5 hover:bg-red-100 focus:ring-2 focus:ring-red-300" onclick="document.getElementById('alert-1').classList.add('hidden')">&times;</button>
</div>

With Flowbite you put data-dismiss-target="#alert-1" on the button and a matching id on the alert. Its Dismiss class adds opacity and transition classes, then applies hidden after the configured duration (Flowbite 2.3.0 source).

Bootstrap’s documentation recommends listening for closed.bs.alert and moving focus somewhere sensible, because a dismissed alert can take keyboard focus with it. Flowbite’s Dismiss accepts an onHide callback that does the same job.

One snag turns up in a GitHub discussion in the htmx repository (#1158). Flowbite’s dismiss button worked on first load but not inside an HTML fragment swapped in later, because the listeners were never attached to the new nodes.

Binding the handler to a parent that survives the swap (event delegation) avoids the problem.

Alpine.js x-show

Alpine.js dismisses an alert with one state value and one directive.

<div x-data="{ open: true }" x-show="open" x-transition role="status" class="flex rounded-lg border border-green-200 bg-green-50 p-4 text-sm text-green-800">
<p>Profile saved.</p>

<button type="button" @click="open = false" aria-label="Close" class="ms-auto rounded-lg p-1.5 hover:bg-green-100">&times;</button>
</div>

The Alpine.js docs state that x-show toggles display: none, and they recommend x-cloak when the default state is hidden, to stop page flicker. Adding x-transition gives a fade for free.

Because x-show only hides the node, the alert stays in the DOM. That suits a live region that gets filled later.

React state

React needs one state flag and nothing else. Start with const [open, setOpen] = useState(true), render the alert only when open is true, and call setOpen(false) from the button’s onClick.

Material Tailwind’s React Alert uses the same shape, controlled through open and onClose props (Material Tailwind docs). In Vue, flowbite-vue’s FwbAlert passes an onCloseClick handler into its default slot.

Unmounting removes the live region along with the alert. If screen reader announcements matter, keep an empty container mounted and render the alert inside it.

How to make a Tailwind alert accessible

Give errors role="alert" and routine messages role="status". The first interrupts the screen reader at once, the second waits for a pause, and neither takes keyboard focus.

MDN Web Docs documents role="alert" as equal to aria-live="assertive" plus aria-atomic="true", and role="status" as aria-live="polite" plus aria-atomic="true" (WAI-ARIA).

Both are ARIA live regions, so stacking an extra aria-live="assertive" on role="alert" is redundant, and MDN notes that combining the two causes double speaking in VoiceOver on iOS. A react-hot-toast issue (#336) cites MDN for exactly that double-speaking problem.

The sources don’t agree on everything, though. MDN reserves alert for content that appears dynamically, yet the Material Tailwind, Flowbite and daisyUI examples put role="alert" on static markup. Vuetify0’s docs warn that an assertive region present at load interrupts the screen reader’s first pass of the page, while a polite status can stay mounted.

GitLab UI keeps role="alert" on its success variant but sets aria-live to polite, and gives info and tip variants role="status" (merge request #3054). HextaUI announces only destructive and warning alerts assertively.

A workable rule is assertive for errors, polite for everything else, and a warning upgraded to alert only when the user must act now.

Injection is the other trap. A report in Freedom Scientific’s standards-support repository (#696) describes JAWS not announcing a role="status" region injected into the DOM, with NVDA behaving the same, while VoiceOver on Safari announced it.

So render the container first and swap the content in, which is the pattern MDN describes. The empty wrapper has no padding, so it takes no space until a message lands inside it.

<div id="flash" role="alert"></div>

<script>
const flash = document.getElementById("flash");

function showError(message) {
  const box = document.createElement("div");

  box.className = "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800";
  box.textContent = message;
  flash.replaceChildren(box);
}
</script>

WCAG 2.2 success criterion 4.1.3, Status Messages (Level AA), requires status messages to be programmatically determinable through role or properties, according to W3C. Messages that move focus, such as an error shown inside a dialog, fall outside it.

Alert copy is body text, so color contrast rules apply to it, which means 4.5:1 for normal text and 3:1 for large text (W3C, Understanding SC 1.4.3).

The W3C adds that computed ratios are not rounded, so 4.499:1 fails. Measure each pale-fill pair from the variants table with a checker instead of trusting the shade numbers.

How to style Tailwind alerts for dark mode and Tailwind CSS v4

In Tailwind CSS v4 the dark: variant follows prefers-color-scheme out of the box. A manual toggle needs one line in your CSS file, because v4 moved configuration into CSS.

@import "tailwindcss";
  @custom-variant dark (&:where(.dark, .dark *));

Left alone, the alert follows the operating system, with no JavaScript and no toggle button. A theme toggle wants the selector approach instead, where a dark class on an ancestor flips everything. To drive it from a data-theme="dark" attribute, swap the selector in the variant line to @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));.

In v3 the same job was a darkMode: 'selector' entry in the config, which replaced the class strategy in v3.4.1 (Tailwind v3 docs). Skip the override in v4 and a .dark class on the html element does nothing, because dark: still listens to the media query.

Each alert variant needs a dark pair. The error variant from the earlier table flips like this.

  • bg-red-50 becomes dark:bg-red-950
  • border-red-200 becomes dark:border-red-900
  • text-red-800 becomes dark:text-red-200

shadcn/ui’s docs use the same idea, adding bg-amber-50 dark:bg-amber-950 to its Alert for a custom amber variant. Run both sets through an accessible color palette generator, since a pair that passes in light mode can fail in dark.

Four variants with three dark classes each get noisy. Semantic tokens define the pairs once.

@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

:root {
  --alert-error-bg: #fef2f2;
  --alert-error-text: #991b1b;
}

.dark {
  --alert-error-bg: #450a0a;
  --alert-error-text: #fecaca;
}

@theme inline {
  --color-alert-error-bg: var(--alert-error-bg);
  --color-alert-error-text: var(--alert-error-text);
}

Variables in the --color-* namespace generate utilities, so --color-alert-error-bg gives you bg-alert-error-bg (Tailwind theme docs). The inline option is what lets a theme variable point at another variable.

The markup then carries no dark: classes at all. For one or two alerts, inline pairs are simpler, and tokens only pay off once every variant repeats the same dark treatment.

Which Tailwind alert library should you use?

Write the utilities yourself for one or two alerts. Reach for a library when four variants, dismissal and dark mode repeat across screens.

LibraryAlert APIDismiss needs JavaScriptFrameworks
FlowbiteHTML markup with Tailwind classesYes, Dismiss classHTML, Vue, Svelte, Angular ports
daisyUIClass names such as alert-errorNot includedAny, pure CSS
shadcn/uiReact components, variant propYou write the stateReact
Preline UIHTML markup, data-hs-remove-element attributeYes, Preline scriptHTML, framework guides
Headless UINo alert componentNot applicableReact, Vue

Headless UI is not an alert library. The React listing on headlessui.com (v2.1 at the time of writing) names sixteen components, from Menu to Textarea, and none is an alert.

What it does offer is Transition for a dismiss animation and Dialog for the blocking case. Preline UI ships solid, soft and bordered alerts with a dismiss attribute, and its script has to be loaded for that attribute to work.

Flowbite

The docs cover info, danger, success, warning and dark alerts, with icon, border and dismissible variants. Ports exist for Vue, Svelte and Angular (flowbite-vue, flowbite-svelte, flowbite-angular), and it works from plain HTML.

The downsides are real, though. Dismissal needs Flowbite’s JavaScript, and the current docs use theme tokens such as bg-danger-soft and text-fg-danger-strong, which are not Tailwind defaults, so a pasted snippet renders unstyled without Flowbite’s theme.

daisyUI

daisyUI 5 is built for Tailwind CSS 4, and its release notes put the full compressed stylesheet at 34 kB, with single component files such as toggle.css loadable on their own.

Alert classes are plain CSS, so no script loads, and alert-vertical or alert-horizontal changes the layout. A responsive form such as alert-vertical sm:alert-horizontal, stacked on phones and side by side from the sm breakpoint, works in v5.

What you don’t get is any dismiss behavior. The class names also hide the utilities, so a tweak means overriding the plugin’s CSS.

shadcn/ui

This is the React option, and it copies the source into your project.

The composition is Alert, AlertTitle, AlertDescription and AlertAction, with a variant prop that accepts default or destructive (shadcn/ui docs). Every other state comes from classes you add yourself.

Since you own the code, dark pairs and variants live in one component. The catch is two built-in variants and a React-only design. Set up Tailwind CSS in React first if the project does not have it yet.

Tailwind Plus

Tailwind Labs designed and built it, and the alert blocks are aimed at billing issues, form validation errors and confirmations. It’s paid, and the markup is copy-paste, so you maintain it.

Worth it when the whole application UI comes from it. Overkill for a single alert box.

When a Tailwind alert is the wrong component

An alert suits a message that should stay visible without stopping the user. It goes wrong when the message must be answered or should vanish on its own, and it goes wrong when several fire together.

  • Short-lived confirmations are toast territory. Tailwind Plus files toasts under Overlays, Notifications, apart from its Feedback alerts, and daisyUI keeps a separate Toast beside Alert.
  • Decisions that block the task call for a modal with role="alertdialog" and focus moved inside it.
  • For field-level errors, inline validation next to the field usually works better than an alert.
  • MDN calls simultaneous and unnecessary alerts a poor experience, so keep one live region and one message at a time.

A toast notification suits confirmations the user can ignore, because it disappears on its own. An alert stays until someone clears it.

Where the line between alert and dialog sits depends on the source. MDN says that if the user is expected to close the alert, use alertdialog instead. The W3C ARIA Authoring Practices Guide keeps alerts away from keyboard focus so the user can keep working, and describes alertdialog as a modal dialog that interrupts the workflow to get a response.

The test that settles it is whether the user has to respond before continuing. A banner they can ignore stays an alert with a close button.

A message that has to be answered becomes a modal dialog, with an accessible name and description wired through aria-labelledby and aria-describedby (MDN). Capturing the focus of the entire page is the hard part, and the Tailwind Labs v1.0 announcement presents Headless UI’s Dialog as the component for exactly that.

Tailwind Alert FAQ

Are Tailwind Plus alert components free?

No. Tailwind Plus is a paid collection from Tailwind Labs.

daisyUI and Preline UI are open-source alternatives, and a basic alert needs only a handful of utility classes anyway.

How do I auto-dismiss a Tailwind alert after a few seconds?

Start a timer when the alert appears and hide the alert when it ends. In Alpine.js, one setTimeout call inside x-init flips the open flag after your chosen delay.

Keep errors on screen until the user closes them, since a timer can remove the message before a screen reader finishes reading it.

Can a Tailwind alert fade in and out?

Yes. Pair transition-opacity and duration-300 with opacity-0, then apply hidden once the duration ends so the element leaves the layout.

Add motion-reduce:transition-none for visitors who ask their operating system for less motion.

How do I make a Tailwind alert full width and responsive?

A flex container already fills its parent. Add w-full only inside shrink-wrapping layouts, and cap the width on large screens with a max-w utility such as max-w-xl.

For narrow screens, stack the icon above the text with flex-col and return to a row using sm:flex-row. Flowbite takes a lighter route, switching alignment with items-start sm:items-center.

Testing a Tailwind Alert with a Screen Reader

A Tailwind alert needs a spoken test in at least two screen readers, NVDA on Windows and VoiceOver on Safari, because a pass in one predicts nothing about the other.

Start with NVDA, since it costs nothing. Move to VoiceOver on Safari after that, and finish on whichever screen reader your own visitors use.

Automated checkers confirm that role=”alert” exists but cannot hear whether the message was spoken, so each of the four variants takes a manual pass. That time is the price of this approach.

Revisit the order whenever NVDA or VoiceOver changes how it treats injected live regions.

Alerts make up one item in a longer audit, and a web accessibility checklist covers the rest of the page.