Pure TailwindMIT licensed

Accessible Tailwind Close Button with Focus Ring (WCAG)

A close button that passes WCAG 2.4.7 and 2.4.11 on any surface: focus-visible:outline-3 plus a two-stop focus-visible:shadow-[…] halo paints ink-on-white-on-ink, so the ring reads on both the light and dark panels. The border and × are currentColor, so one class list serves both themes, and forced-colors: hands the glyph to the system palette in Windows High Contrast.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tcb-02 w-full min-h-svh grid place-items-center bg-tcb02-bg px-6 py-12 font-tcb02-sans" aria-label="Accessible focus ring close button demo">
  <div class="grid w-full justify-items-center gap-7">
    <div class="@container w-full max-w-[30rem]">
      <div class="grid grid-cols-2 overflow-hidden rounded-[1.25rem] shadow-[0_24px_54px_-30px_var(--color-tcb02-shadow)] ring-1 ring-black/5 @max-[22rem]:grid-cols-1">
        <div class="flex flex-col items-center gap-4 bg-white px-5 py-10 text-tcb02-lmut">
          <span class="text-[11px] font-semibold tracking-[0.12em] uppercase">Light surface</span>
          <button type="button" aria-label="Close light panel" class="relative size-11 cursor-pointer rounded-xl border-[1.5px] border-current transition-colors duration-150 before:absolute before:top-1/2 before:left-1/2 before:h-0.5 before:w-4 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-current after:absolute after:top-1/2 after:left-1/2 after:h-0.5 after:w-4 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-current hover:bg-current/10 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tcb02-ring focus-visible:shadow-[0_0_0_6px_white,0_0_0_9px_var(--color-tcb02-ring)] active:scale-95 forced-colors:border-[ButtonText] forced-colors:before:bg-[ButtonText] forced-colors:after:bg-[ButtonText] motion-reduce:transition-none"></button>
          <span class="flex items-center gap-1.5 text-xs">Press <kbd class="rounded-md border border-current/30 px-1.5 py-0.5 font-tcb02-mono text-[11px]">Tab</kbd></span>
        </div>
        <div class="flex flex-col items-center gap-4 bg-tcb02-dark px-5 py-10 text-tcb02-dmut">
          <span class="text-[11px] font-semibold tracking-[0.12em] uppercase">Dark surface</span>
          <button type="button" aria-label="Close dark panel" class="relative size-11 cursor-pointer rounded-xl border-[1.5px] border-current transition-colors duration-150 before:absolute before:top-1/2 before:left-1/2 before:h-0.5 before:w-4 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-current after:absolute after:top-1/2 after:left-1/2 after:h-0.5 after:w-4 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-current hover:bg-current/10 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tcb02-ring focus-visible:shadow-[0_0_0_6px_white,0_0_0_9px_var(--color-tcb02-ring)] active:scale-95 forced-colors:border-[ButtonText] forced-colors:before:bg-[ButtonText] forced-colors:after:bg-[ButtonText] motion-reduce:transition-none"></button>
          <span class="flex items-center gap-1.5 text-xs">then <kbd class="rounded-md border border-current/30 px-1.5 py-0.5 font-tcb02-mono text-[11px]">Tab</kbd> again</span>
        </div>
      </div>
    </div>
    <p aria-hidden="true" class="rounded-full bg-white/70 px-3.5 py-2 font-tcb02-mono text-[11.5px] text-tcb02-lmut ring-1 ring-black/8">focus-visible:outline-3 · shadow halo · forced-colors:</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcb02-bg: #e6e9f0;
  --color-tcb02-bg: oklch(0.93 0.01 260);
  --color-tcb02-dark: #1c2233;
  --color-tcb02-dark: oklch(0.22 0.03 265);
  --color-tcb02-lmut: #4d5163;
  --color-tcb02-lmut: oklch(0.43 0.025 265);
  --color-tcb02-dmut: #c5cad8;
  --color-tcb02-dmut: oklch(0.84 0.02 265);
  --color-tcb02-ring: #3346c9;
  --color-tcb02-ring: oklch(0.45 0.19 265);
  --color-tcb02-shadow: #33384d80;
  --color-tcb02-shadow: oklch(0.35 0.05 265 / 0.5);
  --font-tcb02-sans: 'Geist', system-ui, sans-serif;
  --font-tcb02-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-02 {
  width: 100%;
  min-height: 100svh;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tcb02-bg: #e6e9f0;
  --color-tcb02-bg: oklch(0.93 0.01 260);
  --color-tcb02-dark: #1c2233;
  --color-tcb02-dark: oklch(0.22 0.03 265);
  --color-tcb02-lmut: #4d5163;
  --color-tcb02-lmut: oklch(0.43 0.025 265);
  --color-tcb02-dmut: #c5cad8;
  --color-tcb02-dmut: oklch(0.84 0.02 265);
  --color-tcb02-ring: #3346c9;
  --color-tcb02-ring: oklch(0.45 0.19 265);
  --color-tcb02-shadow: #33384d80;
  --color-tcb02-shadow: oklch(0.35 0.05 265 / 0.5);
  --font-tcb02-sans: 'Geist', system-ui, sans-serif;
  --font-tcb02-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-02 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The @media (forced-colors:active) block became three forced-colors: utilities on the element they change.
  • color-mix(in oklab, currentColor 10%, transparent) became hover:bg-current/10 — the opacity modifier works on currentColor too.
  • UI upgrade from the CSS original: the two panels now carry surface labels and real <kbd>Tab</kbd> hints, each button has a distinct aria-label (the original announced two identical "Close panel" buttons), and the grid stacks under 22rem with a container query.
  • UI upgrade: dark-panel copy rose from oklch(0.8) to 0.84 and light-panel copy darkened to 0.43; both now clear 7:1.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Accessible Tailwind Close Button with Focus Ring (WCAG)
Source: https://codefronts.com/components/tailwind-css-close-buttons/accessible-tailwind-close-button-focus-ring/

A close button that passes WCAG 2.4.7 and 2.4.11 on any surface: focus-visible:outline-3 plus a two-stop focus-visible:shadow-[…] halo paints ink-on-white-on-ink, so the ring reads on both the light and dark panels. The border and × are currentColor, so one class list serves both themes, and forced-colors: hands the glyph to the system palette in Windows High Contrast.
## HTML
```html
<section class="tcb-02 w-full min-h-svh grid place-items-center bg-tcb02-bg px-6 py-12 font-tcb02-sans" aria-label="Accessible focus ring close button demo">
  <div class="grid w-full justify-items-center gap-7">
    <div class="@container w-full max-w-[30rem]">
      <div class="grid grid-cols-2 overflow-hidden rounded-[1.25rem] shadow-[0_24px_54px_-30px_var(--color-tcb02-shadow)] ring-1 ring-black/5 @max-[22rem]:grid-cols-1">
        <div class="flex flex-col items-center gap-4 bg-white px-5 py-10 text-tcb02-lmut">
          <span class="text-[11px] font-semibold tracking-[0.12em] uppercase">Light surface</span>
          <button type="button" aria-label="Close light panel" class="relative size-11 cursor-pointer rounded-xl border-[1.5px] border-current transition-colors duration-150 before:absolute before:top-1/2 before:left-1/2 before:h-0.5 before:w-4 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-current after:absolute after:top-1/2 after:left-1/2 after:h-0.5 after:w-4 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-current hover:bg-current/10 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tcb02-ring focus-visible:shadow-[0_0_0_6px_white,0_0_0_9px_var(--color-tcb02-ring)] active:scale-95 forced-colors:border-[ButtonText] forced-colors:before:bg-[ButtonText] forced-colors:after:bg-[ButtonText] motion-reduce:transition-none"></button>
          <span class="flex items-center gap-1.5 text-xs">Press <kbd class="rounded-md border border-current/30 px-1.5 py-0.5 font-tcb02-mono text-[11px]">Tab</kbd></span>
        </div>
        <div class="flex flex-col items-center gap-4 bg-tcb02-dark px-5 py-10 text-tcb02-dmut">
          <span class="text-[11px] font-semibold tracking-[0.12em] uppercase">Dark surface</span>
          <button type="button" aria-label="Close dark panel" class="relative size-11 cursor-pointer rounded-xl border-[1.5px] border-current transition-colors duration-150 before:absolute before:top-1/2 before:left-1/2 before:h-0.5 before:w-4 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-current after:absolute after:top-1/2 after:left-1/2 after:h-0.5 after:w-4 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-current hover:bg-current/10 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tcb02-ring focus-visible:shadow-[0_0_0_6px_white,0_0_0_9px_var(--color-tcb02-ring)] active:scale-95 forced-colors:border-[ButtonText] forced-colors:before:bg-[ButtonText] forced-colors:after:bg-[ButtonText] motion-reduce:transition-none"></button>
          <span class="flex items-center gap-1.5 text-xs">then <kbd class="rounded-md border border-current/30 px-1.5 py-0.5 font-tcb02-mono text-[11px]">Tab</kbd> again</span>
        </div>
      </div>
    </div>
    <p aria-hidden="true" class="rounded-full bg-white/70 px-3.5 py-2 font-tcb02-mono text-[11.5px] text-tcb02-lmut ring-1 ring-black/8">focus-visible:outline-3 · shadow halo · forced-colors:</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcb02-bg: #e6e9f0;
  --color-tcb02-bg: oklch(0.93 0.01 260);
  --color-tcb02-dark: #1c2233;
  --color-tcb02-dark: oklch(0.22 0.03 265);
  --color-tcb02-lmut: #4d5163;
  --color-tcb02-lmut: oklch(0.43 0.025 265);
  --color-tcb02-dmut: #c5cad8;
  --color-tcb02-dmut: oklch(0.84 0.02 265);
  --color-tcb02-ring: #3346c9;
  --color-tcb02-ring: oklch(0.45 0.19 265);
  --color-tcb02-shadow: #33384d80;
  --color-tcb02-shadow: oklch(0.35 0.05 265 / 0.5);
  --font-tcb02-sans: 'Geist', system-ui, sans-serif;
  --font-tcb02-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-02 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

One ring, three layers. The outline is the primary indicator; the shadow adds a white gap and a second ink ring outside it:

focus-visible:outline-3 focus-visible:outline-offset-2
focus-visible:outline-tcb02-ring
focus-visible:shadow-[0_0_0_6px_white,0_0_0_9px_var(--color-tcb02-ring)]

On white the inner outline carries the contrast; on navy the white gap does. Either way the indicator clears 3:1 against its neighbours, which is what WCAG 2.4.11 measures.

Themes via currentColor. Both panels use the same button markup — the only difference is text-tcb02-lmut vs text-tcb02-dmut on the panel.

Make it yours

  • Tighter ring: drop the shadow and keep outline-3 outline-offset-2 if every surface is light.
  • Square it up: rounded-xl → rounded-full for a circular variant — the ring follows the radius.
  • Brand ring: change --color-tcb02-ring; keep it ≥3:1 against white.

Gotchas — read before shipping

  • focus: would show the ring on mouse click too; focus-visible: only shows it for keyboard and assistive-tech focus.
  • outline-hidden in v4 keeps an invisible outline for forced-colors mode; outline-none removes it — never use either without a replacement.
  • Box-shadow rings are dropped in forced-colors mode, which is why the outline stays the primary indicator.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:focus-visible and forced-colors are universal in evergreen engines; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…