17 / 20
All 20 in 20 Tailwind CSS Popovers
- 01Tailwind Popover Show and Hide with the Popover API
- 02Tailwind Auto vs Manual Popover Behavior
- 03Tailwind Anchored Popover with CSS Anchor Positioning
- 04Tailwind Popover Entry and Exit Animation
- 05Tailwind Popover Backdrop and Light Dismiss
- 06Tailwind Position Area Popover Placement Grid
- 07Tailwind Popover Flip with Position Try Fallbacks
- 08Tailwind Tooltip Arrow Pointing to Anchor
- 09Tailwind Dropdown Matching Anchor Width
- 10Tailwind Nested Popover Submenu
- 11Tailwind Custom Select Listbox Popover
- 12Tailwind Tooltip on Hover and Focus
- 13Tailwind User Profile Dropdown Menu
- 14Tailwind Date Picker Popover
- 15Tailwind Filter Dropdown with Checkboxes
- 16Tailwind Popover Fallback and Polyfill
- 17Tailwind Popover vs Dialog Modal
- 18Tailwind Glassmorphism Popover
- 19Tailwind Dark Mode Popover Toggle
- 20Tailwind Accessible Tooltip with ARIA
Tailwind Popover vs Dialog Modal
One decision rule, shown twice: <dialog> via command="show-modal" when the user must deal with it, popover when they can ignore it and carry on. The port animates both with the same utilities — open: matches [open] on the dialog and :popover-open on the panel — so the comparison isolates modality, and fixes the preflight trap that left the dialog pinned to the top-left.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tpop-17 transition-[--tpop17-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop17-d:1] [[data-theme=dark]_&]:[--tpop17-d:1] [[data-theme=light]_&]:[--tpop17-d:0] group/root w-full min-h-svh bg-tpop17-paper px-4.5 py-10 font-tpop17-sans text-tpop17-ink" aria-label="Popover versus dialog demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[36rem] border border-tpop17-hair bg-tpop17-surface px-5.5 pt-6.5 pb-5.5">
<p class="mb-2 text-[10.5px] font-bold uppercase tracking-[0.22em] text-tpop17-mut">Decision</p>
<h2 class="mb-5.5 font-tpop17-serif text-[clamp(30px,6.5vw,44px)] leading-[1.02] tracking-[-0.01em]">Can the user <em class="italic">ignore</em> it?</h2>
<div class="mb-6 grid grid-cols-[repeat(auto-fit,minmax(min(100%,13rem),1fr))] gap-4.5">
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-go pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-go">Yes → popover</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Page stays live. Focus can leave. An outside click puts it away.</p>
<button id="tpop-17-trigger" type="button" popovertarget="tpop-17-pop" class="min-h-11 cursor-pointer border-2 border-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-ink transition-colors [anchor-name:--tpop17-a] hover:bg-tpop17-go/8 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:border-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:bg-tpop17-go/10">Share this board</button>
</div>
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-stop pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-stop">No → dialog</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Focus contained. Page inert. Backdrop blocks the click.</p>
<button type="button" command="show-modal" commandfor="tpop-17-dialog" class="min-h-11 cursor-pointer border-2 border-tpop17-stop bg-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-surface transition-colors hover:bg-tpop17-stop/90 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete workspace</button>
</div>
</div>
<table class="w-full border-collapse text-[13px]">
<thead><tr><th scope="col" class="pb-2 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-mut"><span class="sr-only">Behaviour</span></th><th scope="col" class="pb-2 pl-3 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-go">Popover</th><th scope="col" class="pb-2 text-right text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-stop">Dialog</th></tr></thead>
<tbody>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Focus</th><td class="py-2 pl-3">free</td><td class="py-2 text-right">contained</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Rest of page</th><td class="py-2 pl-3">interactive</td><td class="py-2 text-right">inert</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Backdrop</th><td class="py-2 pl-3">dismisses</td><td class="py-2 text-right">blocks</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Escape</th><td class="py-2 pl-3">closes top one</td><td class="py-2 text-right">closes, cancelable</td></tr>
</tbody>
</table>
</div>
</div>
<div popover="auto" id="tpop-17-pop" role="group" aria-labelledby="tpop-17-pt" class="fixed m-auto w-[min(20rem,calc(100vw-28px))] border-2 border-tpop17-go bg-tpop17-surface p-4 text-tpop17-ink opacity-0 scale-[0.97] shadow-[6px_6px_0_oklch(0.44_0.08_165/0.22)] transition-[opacity,scale,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop17-a] [position-area:block-end_span-inline-end] [position-try-fallbacks:flip-block,flip-inline] open:scale-100 open:opacity-100 starting:open:scale-[0.97] starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:mt-2.5">
<p id="tpop-17-pt" class="mb-2 text-[11.5px] font-bold uppercase tracking-[0.14em]">Share this board</p>
<p class="mb-3 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Anyone with the link can comment. Keep working — I'm not in your way.</p>
<label for="tpop-17-link" class="sr-only">Share link</label>
<input id="tpop-17-link" readonly value="board.app/s/q3-close-4821" class="mb-3 min-h-11 w-full border border-tpop17-hair bg-tpop17-paper px-3 font-tpop17-mono text-[12.5px] text-tpop17-ink focus-visible:outline-3 focus-visible:-outline-offset-1 focus-visible:outline-tpop17-go">
<p class="border-t border-tpop17-hair pt-2.5 text-[11.5px] font-semibold text-tpop17-go">popover · non-modal</p>
</div>
<dialog id="tpop-17-dialog" aria-labelledby="tpop-17-dt" class="m-auto w-[min(24rem,calc(100vw-32px))] border-2 border-tpop17-stop bg-tpop17-surface p-5 text-tpop17-ink opacity-0 scale-95 transition-[opacity,scale,display,overlay] duration-200 ease-out transition-discrete open:scale-100 open:opacity-100 starting:open:scale-95 starting:open:opacity-0 backdrop:bg-tpop17-ink/0 backdrop:transition-[background-color,display,overlay] backdrop:duration-200 backdrop:transition-discrete open:backdrop:bg-tpop17-ink/55 starting:open:backdrop:bg-tpop17-ink/0 motion-reduce:transition-none motion-reduce:backdrop:transition-none">
<p id="tpop-17-dt" class="mb-2 font-tpop17-serif text-[26px] leading-tight">Delete workspace?</p>
<p class="mb-4 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">This removes <strong class="font-semibold text-tpop17-ink">1,284 records</strong> and cannot be undone. You must answer before continuing.</p>
<div class="flex flex-wrap gap-2">
<button type="button" autofocus command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-ink bg-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go">Cancel</button>
<button type="button" command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-stop transition-colors hover:bg-tpop17-stop hover:text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete</button>
</div>
</dialog>
</section><section class="tpop-17 transition-[--tpop17-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop17-d:1] [[data-theme=dark]_&]:[--tpop17-d:1] [[data-theme=light]_&]:[--tpop17-d:0] group/root w-full min-h-svh bg-tpop17-paper px-4.5 py-10 font-tpop17-sans text-tpop17-ink" aria-label="Popover versus dialog demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[36rem] border border-tpop17-hair bg-tpop17-surface px-5.5 pt-6.5 pb-5.5">
<p class="mb-2 text-[10.5px] font-bold uppercase tracking-[0.22em] text-tpop17-mut">Decision</p>
<h2 class="mb-5.5 font-tpop17-serif text-[clamp(30px,6.5vw,44px)] leading-[1.02] tracking-[-0.01em]">Can the user <em class="italic">ignore</em> it?</h2>
<div class="mb-6 grid grid-cols-[repeat(auto-fit,minmax(min(100%,13rem),1fr))] gap-4.5">
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-go pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-go">Yes → popover</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Page stays live. Focus can leave. An outside click puts it away.</p>
<button id="tpop-17-trigger" type="button" popovertarget="tpop-17-pop" class="min-h-11 cursor-pointer border-2 border-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-ink transition-colors [anchor-name:--tpop17-a] hover:bg-tpop17-go/8 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:border-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:bg-tpop17-go/10">Share this board</button>
</div>
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-stop pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-stop">No → dialog</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Focus contained. Page inert. Backdrop blocks the click.</p>
<button type="button" command="show-modal" commandfor="tpop-17-dialog" class="min-h-11 cursor-pointer border-2 border-tpop17-stop bg-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-surface transition-colors hover:bg-tpop17-stop/90 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete workspace</button>
</div>
</div>
<table class="w-full border-collapse text-[13px]">
<thead><tr><th scope="col" class="pb-2 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-mut"><span class="sr-only">Behaviour</span></th><th scope="col" class="pb-2 pl-3 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-go">Popover</th><th scope="col" class="pb-2 text-right text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-stop">Dialog</th></tr></thead>
<tbody>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Focus</th><td class="py-2 pl-3">free</td><td class="py-2 text-right">contained</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Rest of page</th><td class="py-2 pl-3">interactive</td><td class="py-2 text-right">inert</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Backdrop</th><td class="py-2 pl-3">dismisses</td><td class="py-2 text-right">blocks</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Escape</th><td class="py-2 pl-3">closes top one</td><td class="py-2 text-right">closes, cancelable</td></tr>
</tbody>
</table>
</div>
</div>
<div popover="auto" id="tpop-17-pop" role="group" aria-labelledby="tpop-17-pt" class="fixed m-auto w-[min(20rem,calc(100vw-28px))] border-2 border-tpop17-go bg-tpop17-surface p-4 text-tpop17-ink opacity-0 scale-[0.97] shadow-[6px_6px_0_oklch(0.44_0.08_165/0.22)] transition-[opacity,scale,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop17-a] [position-area:block-end_span-inline-end] [position-try-fallbacks:flip-block,flip-inline] open:scale-100 open:opacity-100 starting:open:scale-[0.97] starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:mt-2.5">
<p id="tpop-17-pt" class="mb-2 text-[11.5px] font-bold uppercase tracking-[0.14em]">Share this board</p>
<p class="mb-3 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Anyone with the link can comment. Keep working — I'm not in your way.</p>
<label for="tpop-17-link" class="sr-only">Share link</label>
<input id="tpop-17-link" readonly value="board.app/s/q3-close-4821" class="mb-3 min-h-11 w-full border border-tpop17-hair bg-tpop17-paper px-3 font-tpop17-mono text-[12.5px] text-tpop17-ink focus-visible:outline-3 focus-visible:-outline-offset-1 focus-visible:outline-tpop17-go">
<p class="border-t border-tpop17-hair pt-2.5 text-[11.5px] font-semibold text-tpop17-go">popover · non-modal</p>
</div>
<dialog id="tpop-17-dialog" aria-labelledby="tpop-17-dt" class="m-auto w-[min(24rem,calc(100vw-32px))] border-2 border-tpop17-stop bg-tpop17-surface p-5 text-tpop17-ink opacity-0 scale-95 transition-[opacity,scale,display,overlay] duration-200 ease-out transition-discrete open:scale-100 open:opacity-100 starting:open:scale-95 starting:open:opacity-0 backdrop:bg-tpop17-ink/0 backdrop:transition-[background-color,display,overlay] backdrop:duration-200 backdrop:transition-discrete open:backdrop:bg-tpop17-ink/55 starting:open:backdrop:bg-tpop17-ink/0 motion-reduce:transition-none motion-reduce:backdrop:transition-none">
<p id="tpop-17-dt" class="mb-2 font-tpop17-serif text-[26px] leading-tight">Delete workspace?</p>
<p class="mb-4 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">This removes <strong class="font-semibold text-tpop17-ink">1,284 records</strong> and cannot be undone. You must answer before continuing.</p>
<div class="flex flex-wrap gap-2">
<button type="button" autofocus command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-ink bg-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go">Cancel</button>
<button type="button" command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-stop transition-colors hover:bg-tpop17-stop hover:text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete</button>
</div>
</dialog>
</section>@import "tailwindcss";
@theme {
--color-tpop17-paper: #f5f3ed;
--color-tpop17-paper: oklch(0.965 0.008 90);
--color-tpop17-surface: #ffffff;
--color-tpop17-surface: oklch(1 0 0);
--color-tpop17-ink: #151410;
--color-tpop17-ink: oklch(0.19 0.008 90);
--color-tpop17-mut: #5f5a51;
--color-tpop17-mut: oklch(0.47 0.015 85);
--color-tpop17-hair: #dedad2;
--color-tpop17-hair: oklch(0.89 0.012 85);
--color-tpop17-go: #1b6047;
--color-tpop17-go: oklch(0.44 0.08 165);
--color-tpop17-stop: #9d2f19;
--color-tpop17-stop: oklch(0.47 0.15 33);
--font-tpop17-serif: 'Instrument Serif', Georgia, serif;
--font-tpop17-sans: 'Geist', system-ui, sans-serif;
--font-tpop17-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop17-d {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* 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 — plus the dark palette. Each @theme colour is
re-declared on the root as a light/dark mix of the dial, so every utility (and every open
popover, which inherits from the root) follows it. Light value last, dark value first. */
.tpop-17 {
width: 100%;
min-height: 100svh;
--color-tpop17-paper: color-mix(in oklch, oklch(0.16 0.008 85) calc(var(--tpop17-d) * 100%), oklch(0.965 0.008 90));
--color-tpop17-surface: color-mix(in oklch, oklch(0.205 0.01 85) calc(var(--tpop17-d) * 100%), oklch(1 0 0));
--color-tpop17-ink: color-mix(in oklch, oklch(0.95 0.01 90) calc(var(--tpop17-d) * 100%), oklch(0.19 0.008 90));
--color-tpop17-mut: color-mix(in oklch, oklch(0.73 0.015 85) calc(var(--tpop17-d) * 100%), oklch(0.47 0.015 85));
--color-tpop17-hair: color-mix(in oklch, oklch(0.3 0.012 85) calc(var(--tpop17-d) * 100%), oklch(0.89 0.012 85));
--color-tpop17-go: color-mix(in oklch, oklch(0.76 0.1 165) calc(var(--tpop17-d) * 100%), oklch(0.44 0.08 165));
--color-tpop17-stop: color-mix(in oklch, oklch(0.74 0.14 33) calc(var(--tpop17-d) * 100%), oklch(0.47 0.15 33));
}@import "tailwindcss";
@theme {
--color-tpop17-paper: #f5f3ed;
--color-tpop17-paper: oklch(0.965 0.008 90);
--color-tpop17-surface: #ffffff;
--color-tpop17-surface: oklch(1 0 0);
--color-tpop17-ink: #151410;
--color-tpop17-ink: oklch(0.19 0.008 90);
--color-tpop17-mut: #5f5a51;
--color-tpop17-mut: oklch(0.47 0.015 85);
--color-tpop17-hair: #dedad2;
--color-tpop17-hair: oklch(0.89 0.012 85);
--color-tpop17-go: #1b6047;
--color-tpop17-go: oklch(0.44 0.08 165);
--color-tpop17-stop: #9d2f19;
--color-tpop17-stop: oklch(0.47 0.15 33);
--font-tpop17-serif: 'Instrument Serif', Georgia, serif;
--font-tpop17-sans: 'Geist', system-ui, sans-serif;
--font-tpop17-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop17-d {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* 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 — plus the dark palette. Each @theme colour is
re-declared on the root as a light/dark mix of the dial, so every utility (and every open
popover, which inherits from the root) follows it. Light value last, dark value first. */
.tpop-17 {
width: 100%;
min-height: 100svh;
--color-tpop17-paper: color-mix(in oklch, oklch(0.16 0.008 85) calc(var(--tpop17-d) * 100%), oklch(0.965 0.008 90));
--color-tpop17-surface: color-mix(in oklch, oklch(0.205 0.01 85) calc(var(--tpop17-d) * 100%), oklch(1 0 0));
--color-tpop17-ink: color-mix(in oklch, oklch(0.95 0.01 90) calc(var(--tpop17-d) * 100%), oklch(0.19 0.008 90));
--color-tpop17-mut: color-mix(in oklch, oklch(0.73 0.015 85) calc(var(--tpop17-d) * 100%), oklch(0.47 0.015 85));
--color-tpop17-hair: color-mix(in oklch, oklch(0.3 0.012 85) calc(var(--tpop17-d) * 100%), oklch(0.89 0.012 85));
--color-tpop17-go: color-mix(in oklch, oklch(0.76 0.1 165) calc(var(--tpop17-d) * 100%), oklch(0.44 0.08 165));
--color-tpop17-stop: color-mix(in oklch, oklch(0.74 0.14 33) calc(var(--tpop17-d) * 100%), oklch(0.47 0.15 33));
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tpop17-paper: #f5f3ed;
--color-tpop17-paper: oklch(0.965 0.008 90);
--color-tpop17-surface: #ffffff;
--color-tpop17-surface: oklch(1 0 0);
--color-tpop17-ink: #151410;
--color-tpop17-ink: oklch(0.19 0.008 90);
--color-tpop17-mut: #5f5a51;
--color-tpop17-mut: oklch(0.47 0.015 85);
--color-tpop17-hair: #dedad2;
--color-tpop17-hair: oklch(0.89 0.012 85);
--color-tpop17-go: #1b6047;
--color-tpop17-go: oklch(0.44 0.08 165);
--color-tpop17-stop: #9d2f19;
--color-tpop17-stop: oklch(0.47 0.15 33);
--font-tpop17-serif: 'Instrument Serif', Georgia, serif;
--font-tpop17-sans: 'Geist', system-ui, sans-serif;
--font-tpop17-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop17-d {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* 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 — plus the dark palette. Each @theme colour is
re-declared on the root as a light/dark mix of the dial, so every utility (and every open
popover, which inherits from the root) follows it. Light value last, dark value first. */
.tpop-17 {
width: 100%;
min-height: 100svh;
--color-tpop17-paper: color-mix(in oklch, oklch(0.16 0.008 85) calc(var(--tpop17-d) * 100%), oklch(0.965 0.008 90));
--color-tpop17-surface: color-mix(in oklch, oklch(0.205 0.01 85) calc(var(--tpop17-d) * 100%), oklch(1 0 0));
--color-tpop17-ink: color-mix(in oklch, oklch(0.95 0.01 90) calc(var(--tpop17-d) * 100%), oklch(0.19 0.008 90));
--color-tpop17-mut: color-mix(in oklch, oklch(0.73 0.015 85) calc(var(--tpop17-d) * 100%), oklch(0.47 0.015 85));
--color-tpop17-hair: color-mix(in oklch, oklch(0.3 0.012 85) calc(var(--tpop17-d) * 100%), oklch(0.89 0.012 85));
--color-tpop17-go: color-mix(in oklch, oklch(0.76 0.1 165) calc(var(--tpop17-d) * 100%), oklch(0.44 0.08 165));
--color-tpop17-stop: color-mix(in oklch, oklch(0.74 0.14 33) calc(var(--tpop17-d) * 100%), oklch(0.47 0.15 33));
}@import "tailwindcss";
@theme {
--color-tpop17-paper: #f5f3ed;
--color-tpop17-paper: oklch(0.965 0.008 90);
--color-tpop17-surface: #ffffff;
--color-tpop17-surface: oklch(1 0 0);
--color-tpop17-ink: #151410;
--color-tpop17-ink: oklch(0.19 0.008 90);
--color-tpop17-mut: #5f5a51;
--color-tpop17-mut: oklch(0.47 0.015 85);
--color-tpop17-hair: #dedad2;
--color-tpop17-hair: oklch(0.89 0.012 85);
--color-tpop17-go: #1b6047;
--color-tpop17-go: oklch(0.44 0.08 165);
--color-tpop17-stop: #9d2f19;
--color-tpop17-stop: oklch(0.47 0.15 33);
--font-tpop17-serif: 'Instrument Serif', Georgia, serif;
--font-tpop17-sans: 'Geist', system-ui, sans-serif;
--font-tpop17-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop17-d {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* 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 — plus the dark palette. Each @theme colour is
re-declared on the root as a light/dark mix of the dial, so every utility (and every open
popover, which inherits from the root) follows it. Light value last, dark value first. */
.tpop-17 {
width: 100%;
min-height: 100svh;
--color-tpop17-paper: color-mix(in oklch, oklch(0.16 0.008 85) calc(var(--tpop17-d) * 100%), oklch(0.965 0.008 90));
--color-tpop17-surface: color-mix(in oklch, oklch(0.205 0.01 85) calc(var(--tpop17-d) * 100%), oklch(1 0 0));
--color-tpop17-ink: color-mix(in oklch, oklch(0.95 0.01 90) calc(var(--tpop17-d) * 100%), oklch(0.19 0.008 90));
--color-tpop17-mut: color-mix(in oklch, oklch(0.73 0.015 85) calc(var(--tpop17-d) * 100%), oklch(0.47 0.015 85));
--color-tpop17-hair: color-mix(in oklch, oklch(0.3 0.012 85) calc(var(--tpop17-d) * 100%), oklch(0.89 0.012 85));
--color-tpop17-go: color-mix(in oklch, oklch(0.76 0.1 165) calc(var(--tpop17-d) * 100%), oklch(0.44 0.08 165));
--color-tpop17-stop: color-mix(in oklch, oklch(0.74 0.14 33) calc(var(--tpop17-d) * 100%), oklch(0.47 0.15 33));
}<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=Instrument+Serif:ital@0;1&family=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">
<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=Instrument+Serif:ital@0;1&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.
.pop-17__td:first-of-type/:last-of-typecolouring became a proper table header row naming the two columns — structure instead of positional selectors.- The dialog's
::backdroprule becamebackdrop:,open:backdrop:andstarting:open:backdrop:utilities, mirroring demo 05. - Bug fixes from the CSS original: (1)
anchor-namesat on every.pop-17__btn, including the buttons inside the dialog, so the share panel's anchor was ambiguous — only the Share button carries it now; (2) under Tailwind's preflight the dialog would lose its UAmargin: auto—m-autorestores it. - UI upgrade: both surfaces now animate in and out with one shared recipe, the modal backdrop fades, Cancel gets
autofocusas the safe default, Delete is visibly destructive, and the share panel gained a copyable link field. - UI upgrade — dark mode: the stage now follows
prefers-color-scheme(and adata-themeoverride on any ancestor) through one registered dial,--tpop17-d. The root rule re-declares each@themecolour ascolor-mix(dark calc(var(--tpop17-d)*100%), light), the root sets the dial withdark:[--tpop17-d:1], andtransition-[--tpop17-d]crossfades page and open popovers together — the markup's colour utilities are unchanged.
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: Tailwind Popover vs Dialog Modal
Source: https://codefronts.com/snippets/tailwind-css-popovers/tailwind-popover-vs-dialog-modal/
One decision rule, shown twice: <dialog> via command="show-modal" when the user must deal with it, popover when they can ignore it and carry on. The port animates both with the same utilities — open: matches [open] on the dialog and :popover-open on the panel — so the comparison isolates modality, and fixes the preflight trap that left the dialog pinned to the top-left.
## HTML
```html
<section class="tpop-17 transition-[--tpop17-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop17-d:1] [[data-theme=dark]_&]:[--tpop17-d:1] [[data-theme=light]_&]:[--tpop17-d:0] group/root w-full min-h-svh bg-tpop17-paper px-4.5 py-10 font-tpop17-sans text-tpop17-ink" aria-label="Popover versus dialog demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[36rem] border border-tpop17-hair bg-tpop17-surface px-5.5 pt-6.5 pb-5.5">
<p class="mb-2 text-[10.5px] font-bold uppercase tracking-[0.22em] text-tpop17-mut">Decision</p>
<h2 class="mb-5.5 font-tpop17-serif text-[clamp(30px,6.5vw,44px)] leading-[1.02] tracking-[-0.01em]">Can the user <em class="italic">ignore</em> it?</h2>
<div class="mb-6 grid grid-cols-[repeat(auto-fit,minmax(min(100%,13rem),1fr))] gap-4.5">
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-go pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-go">Yes → popover</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Page stays live. Focus can leave. An outside click puts it away.</p>
<button id="tpop-17-trigger" type="button" popovertarget="tpop-17-pop" class="min-h-11 cursor-pointer border-2 border-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-ink transition-colors [anchor-name:--tpop17-a] hover:bg-tpop17-go/8 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:border-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:bg-tpop17-go/10">Share this board</button>
</div>
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-stop pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-stop">No → dialog</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Focus contained. Page inert. Backdrop blocks the click.</p>
<button type="button" command="show-modal" commandfor="tpop-17-dialog" class="min-h-11 cursor-pointer border-2 border-tpop17-stop bg-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-surface transition-colors hover:bg-tpop17-stop/90 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete workspace</button>
</div>
</div>
<table class="w-full border-collapse text-[13px]">
<thead><tr><th scope="col" class="pb-2 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-mut"><span class="sr-only">Behaviour</span></th><th scope="col" class="pb-2 pl-3 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-go">Popover</th><th scope="col" class="pb-2 text-right text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-stop">Dialog</th></tr></thead>
<tbody>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Focus</th><td class="py-2 pl-3">free</td><td class="py-2 text-right">contained</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Rest of page</th><td class="py-2 pl-3">interactive</td><td class="py-2 text-right">inert</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Backdrop</th><td class="py-2 pl-3">dismisses</td><td class="py-2 text-right">blocks</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Escape</th><td class="py-2 pl-3">closes top one</td><td class="py-2 text-right">closes, cancelable</td></tr>
</tbody>
</table>
</div>
</div>
<div popover="auto" id="tpop-17-pop" role="group" aria-labelledby="tpop-17-pt" class="fixed m-auto w-[min(20rem,calc(100vw-28px))] border-2 border-tpop17-go bg-tpop17-surface p-4 text-tpop17-ink opacity-0 scale-[0.97] shadow-[6px_6px_0_oklch(0.44_0.08_165/0.22)] transition-[opacity,scale,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop17-a] [position-area:block-end_span-inline-end] [position-try-fallbacks:flip-block,flip-inline] open:scale-100 open:opacity-100 starting:open:scale-[0.97] starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:mt-2.5">
<p id="tpop-17-pt" class="mb-2 text-[11.5px] font-bold uppercase tracking-[0.14em]">Share this board</p>
<p class="mb-3 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Anyone with the link can comment. Keep working — I'm not in your way.</p>
<label for="tpop-17-link" class="sr-only">Share link</label>
<input id="tpop-17-link" readonly value="board.app/s/q3-close-4821" class="mb-3 min-h-11 w-full border border-tpop17-hair bg-tpop17-paper px-3 font-tpop17-mono text-[12.5px] text-tpop17-ink focus-visible:outline-3 focus-visible:-outline-offset-1 focus-visible:outline-tpop17-go">
<p class="border-t border-tpop17-hair pt-2.5 text-[11.5px] font-semibold text-tpop17-go">popover · non-modal</p>
</div>
<dialog id="tpop-17-dialog" aria-labelledby="tpop-17-dt" class="m-auto w-[min(24rem,calc(100vw-32px))] border-2 border-tpop17-stop bg-tpop17-surface p-5 text-tpop17-ink opacity-0 scale-95 transition-[opacity,scale,display,overlay] duration-200 ease-out transition-discrete open:scale-100 open:opacity-100 starting:open:scale-95 starting:open:opacity-0 backdrop:bg-tpop17-ink/0 backdrop:transition-[background-color,display,overlay] backdrop:duration-200 backdrop:transition-discrete open:backdrop:bg-tpop17-ink/55 starting:open:backdrop:bg-tpop17-ink/0 motion-reduce:transition-none motion-reduce:backdrop:transition-none">
<p id="tpop-17-dt" class="mb-2 font-tpop17-serif text-[26px] leading-tight">Delete workspace?</p>
<p class="mb-4 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">This removes <strong class="font-semibold text-tpop17-ink">1,284 records</strong> and cannot be undone. You must answer before continuing.</p>
<div class="flex flex-wrap gap-2">
<button type="button" autofocus command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-ink bg-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go">Cancel</button>
<button type="button" command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-stop transition-colors hover:bg-tpop17-stop hover:text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete</button>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpop17-paper: #f5f3ed;
--color-tpop17-paper: oklch(0.965 0.008 90);
--color-tpop17-surface: #ffffff;
--color-tpop17-surface: oklch(1 0 0);
--color-tpop17-ink: #151410;
--color-tpop17-ink: oklch(0.19 0.008 90);
--color-tpop17-mut: #5f5a51;
--color-tpop17-mut: oklch(0.47 0.015 85);
--color-tpop17-hair: #dedad2;
--color-tpop17-hair: oklch(0.89 0.012 85);
--color-tpop17-go: #1b6047;
--color-tpop17-go: oklch(0.44 0.08 165);
--color-tpop17-stop: #9d2f19;
--color-tpop17-stop: oklch(0.47 0.15 33);
--font-tpop17-serif: 'Instrument Serif', Georgia, serif;
--font-tpop17-sans: 'Geist', system-ui, sans-serif;
--font-tpop17-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop17-d {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* 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 — plus the dark palette. Each @theme colour is
re-declared on the root as a light/dark mix of the dial, so every utility (and every open
popover, which inherits from the root) follows it. Light value last, dark value first. */
.tpop-17 {
width: 100%;
min-height: 100svh;
--color-tpop17-paper: color-mix(in oklch, oklch(0.16 0.008 85) calc(var(--tpop17-d) * 100%), oklch(0.965 0.008 90));
--color-tpop17-surface: color-mix(in oklch, oklch(0.205 0.01 85) calc(var(--tpop17-d) * 100%), oklch(1 0 0));
--color-tpop17-ink: color-mix(in oklch, oklch(0.95 0.01 90) calc(var(--tpop17-d) * 100%), oklch(0.19 0.008 90));
--color-tpop17-mut: color-mix(in oklch, oklch(0.73 0.015 85) calc(var(--tpop17-d) * 100%), oklch(0.47 0.015 85));
--color-tpop17-hair: color-mix(in oklch, oklch(0.3 0.012 85) calc(var(--tpop17-d) * 100%), oklch(0.89 0.012 85));
--color-tpop17-go: color-mix(in oklch, oklch(0.76 0.1 165) calc(var(--tpop17-d) * 100%), oklch(0.44 0.08 165));
--color-tpop17-stop: color-mix(in oklch, oklch(0.74 0.14 33) calc(var(--tpop17-d) * 100%), oklch(0.47 0.15 33));
}
```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: Tailwind Popover vs Dialog Modal
Source: https://codefronts.com/snippets/tailwind-css-popovers/tailwind-popover-vs-dialog-modal/
One decision rule, shown twice: <dialog> via command="show-modal" when the user must deal with it, popover when they can ignore it and carry on. The port animates both with the same utilities — open: matches [open] on the dialog and :popover-open on the panel — so the comparison isolates modality, and fixes the preflight trap that left the dialog pinned to the top-left.
## HTML
```html
<section class="tpop-17 transition-[--tpop17-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop17-d:1] [[data-theme=dark]_&]:[--tpop17-d:1] [[data-theme=light]_&]:[--tpop17-d:0] group/root w-full min-h-svh bg-tpop17-paper px-4.5 py-10 font-tpop17-sans text-tpop17-ink" aria-label="Popover versus dialog demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[36rem] border border-tpop17-hair bg-tpop17-surface px-5.5 pt-6.5 pb-5.5">
<p class="mb-2 text-[10.5px] font-bold uppercase tracking-[0.22em] text-tpop17-mut">Decision</p>
<h2 class="mb-5.5 font-tpop17-serif text-[clamp(30px,6.5vw,44px)] leading-[1.02] tracking-[-0.01em]">Can the user <em class="italic">ignore</em> it?</h2>
<div class="mb-6 grid grid-cols-[repeat(auto-fit,minmax(min(100%,13rem),1fr))] gap-4.5">
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-go pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-go">Yes → popover</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Page stays live. Focus can leave. An outside click puts it away.</p>
<button id="tpop-17-trigger" type="button" popovertarget="tpop-17-pop" class="min-h-11 cursor-pointer border-2 border-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-ink transition-colors [anchor-name:--tpop17-a] hover:bg-tpop17-go/8 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:border-tpop17-go group-has-[#tpop-17-pop:popover-open]/root:bg-tpop17-go/10">Share this board</button>
</div>
<div class="grid min-w-0 content-start gap-2 border-t-2 border-tpop17-stop pt-3">
<p class="text-xs font-bold uppercase tracking-[0.08em] text-tpop17-stop">No → dialog</p>
<p class="mb-1.5 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Focus contained. Page inert. Backdrop blocks the click.</p>
<button type="button" command="show-modal" commandfor="tpop-17-dialog" class="min-h-11 cursor-pointer border-2 border-tpop17-stop bg-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-surface transition-colors hover:bg-tpop17-stop/90 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete workspace</button>
</div>
</div>
<table class="w-full border-collapse text-[13px]">
<thead><tr><th scope="col" class="pb-2 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-mut"><span class="sr-only">Behaviour</span></th><th scope="col" class="pb-2 pl-3 text-left text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-go">Popover</th><th scope="col" class="pb-2 text-right text-[10.5px] font-bold uppercase tracking-[0.14em] text-tpop17-stop">Dialog</th></tr></thead>
<tbody>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Focus</th><td class="py-2 pl-3">free</td><td class="py-2 text-right">contained</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Rest of page</th><td class="py-2 pl-3">interactive</td><td class="py-2 text-right">inert</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Backdrop</th><td class="py-2 pl-3">dismisses</td><td class="py-2 text-right">blocks</td></tr>
<tr class="border-t border-tpop17-hair"><th scope="row" class="py-2 text-left font-semibold text-tpop17-mut">Escape</th><td class="py-2 pl-3">closes top one</td><td class="py-2 text-right">closes, cancelable</td></tr>
</tbody>
</table>
</div>
</div>
<div popover="auto" id="tpop-17-pop" role="group" aria-labelledby="tpop-17-pt" class="fixed m-auto w-[min(20rem,calc(100vw-28px))] border-2 border-tpop17-go bg-tpop17-surface p-4 text-tpop17-ink opacity-0 scale-[0.97] shadow-[6px_6px_0_oklch(0.44_0.08_165/0.22)] transition-[opacity,scale,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop17-a] [position-area:block-end_span-inline-end] [position-try-fallbacks:flip-block,flip-inline] open:scale-100 open:opacity-100 starting:open:scale-[0.97] starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:mt-2.5">
<p id="tpop-17-pt" class="mb-2 text-[11.5px] font-bold uppercase tracking-[0.14em]">Share this board</p>
<p class="mb-3 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">Anyone with the link can comment. Keep working — I'm not in your way.</p>
<label for="tpop-17-link" class="sr-only">Share link</label>
<input id="tpop-17-link" readonly value="board.app/s/q3-close-4821" class="mb-3 min-h-11 w-full border border-tpop17-hair bg-tpop17-paper px-3 font-tpop17-mono text-[12.5px] text-tpop17-ink focus-visible:outline-3 focus-visible:-outline-offset-1 focus-visible:outline-tpop17-go">
<p class="border-t border-tpop17-hair pt-2.5 text-[11.5px] font-semibold text-tpop17-go">popover · non-modal</p>
</div>
<dialog id="tpop-17-dialog" aria-labelledby="tpop-17-dt" class="m-auto w-[min(24rem,calc(100vw-32px))] border-2 border-tpop17-stop bg-tpop17-surface p-5 text-tpop17-ink opacity-0 scale-95 transition-[opacity,scale,display,overlay] duration-200 ease-out transition-discrete open:scale-100 open:opacity-100 starting:open:scale-95 starting:open:opacity-0 backdrop:bg-tpop17-ink/0 backdrop:transition-[background-color,display,overlay] backdrop:duration-200 backdrop:transition-discrete open:backdrop:bg-tpop17-ink/55 starting:open:backdrop:bg-tpop17-ink/0 motion-reduce:transition-none motion-reduce:backdrop:transition-none">
<p id="tpop-17-dt" class="mb-2 font-tpop17-serif text-[26px] leading-tight">Delete workspace?</p>
<p class="mb-4 text-[13.5px] leading-relaxed text-tpop17-mut text-pretty">This removes <strong class="font-semibold text-tpop17-ink">1,284 records</strong> and cannot be undone. You must answer before continuing.</p>
<div class="flex flex-wrap gap-2">
<button type="button" autofocus command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-ink bg-tpop17-ink px-3.5 text-[13px] font-semibold text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-go">Cancel</button>
<button type="button" command="close" commandfor="tpop-17-dialog" class="min-h-11 min-w-0 flex-auto cursor-pointer border-2 border-tpop17-stop px-3.5 text-[13px] font-semibold text-tpop17-stop transition-colors hover:bg-tpop17-stop hover:text-tpop17-surface focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop17-stop">Delete</button>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpop17-paper: #f5f3ed;
--color-tpop17-paper: oklch(0.965 0.008 90);
--color-tpop17-surface: #ffffff;
--color-tpop17-surface: oklch(1 0 0);
--color-tpop17-ink: #151410;
--color-tpop17-ink: oklch(0.19 0.008 90);
--color-tpop17-mut: #5f5a51;
--color-tpop17-mut: oklch(0.47 0.015 85);
--color-tpop17-hair: #dedad2;
--color-tpop17-hair: oklch(0.89 0.012 85);
--color-tpop17-go: #1b6047;
--color-tpop17-go: oklch(0.44 0.08 165);
--color-tpop17-stop: #9d2f19;
--color-tpop17-stop: oklch(0.47 0.15 33);
--font-tpop17-serif: 'Instrument Serif', Georgia, serif;
--font-tpop17-sans: 'Geist', system-ui, sans-serif;
--font-tpop17-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop17-d {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* 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 — plus the dark palette. Each @theme colour is
re-declared on the root as a light/dark mix of the dial, so every utility (and every open
popover, which inherits from the root) follows it. Light value last, dark value first. */
.tpop-17 {
width: 100%;
min-height: 100svh;
--color-tpop17-paper: color-mix(in oklch, oklch(0.16 0.008 85) calc(var(--tpop17-d) * 100%), oklch(0.965 0.008 90));
--color-tpop17-surface: color-mix(in oklch, oklch(0.205 0.01 85) calc(var(--tpop17-d) * 100%), oklch(1 0 0));
--color-tpop17-ink: color-mix(in oklch, oklch(0.95 0.01 90) calc(var(--tpop17-d) * 100%), oklch(0.19 0.008 90));
--color-tpop17-mut: color-mix(in oklch, oklch(0.73 0.015 85) calc(var(--tpop17-d) * 100%), oklch(0.47 0.015 85));
--color-tpop17-hair: color-mix(in oklch, oklch(0.3 0.012 85) calc(var(--tpop17-d) * 100%), oklch(0.89 0.012 85));
--color-tpop17-go: color-mix(in oklch, oklch(0.76 0.1 165) calc(var(--tpop17-d) * 100%), oklch(0.44 0.08 165));
--color-tpop17-stop: color-mix(in oklch, oklch(0.74 0.14 33) calc(var(--tpop17-d) * 100%), oklch(0.47 0.15 33));
}
```How this works
Ask one question: can the user ignore this? Yes — a share sheet, a hint, a filter — is a popover. No — a destructive confirmation — is a dialog opened modally. Focus containment, page inertness and a click-blocking backdrop all follow from that answer.
<button command="show-modal" commandfor="tpop-17-dialog">Delete workspace</button>
<dialog id="tpop-17-dialog" class="m-auto … opacity-0 scale-95
transition-[opacity,scale,display,overlay] transition-discrete
open:opacity-100 open:scale-100
starting:open:opacity-0 starting:open:scale-95
backdrop:bg-tpop17-ink/0 open:backdrop:bg-tpop17-ink/55">Same variant, two elements. v4's open: compiles to :is([open], :popover-open, :open), so the dialog and the popover share an identical entry/exit recipe. What differs is what happens behind them: the dialog's backdrop swallows the click; the popover's backdrop-less page stays live.
Don't trap focus in a popover. If you're writing a focus trap, you wanted a dialog.
Make it yours
- Feel a non-modal dialog: change
command="show-modal"toshow— then delete it and use the popover. - Blur both: add
backdrop:backdrop-blur-smto the dialog to show the visual overlap. - Require input: add a text field that must read the workspace name before Delete enables.
Gotchas — read before shipping
- Preflight's
margin: 0un-centres<dialog>too — restorem-auto. - A non-modal
show()dialog has no backdrop or inertness — a popover with extra steps. command/commandforare newer than both elements; older engines need oneshowModal()line.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 135+ | 18.4+ | 137+ | 135+ |
The floor is the declarative invoker (command/commandfor). <dialog> is far older and popover is Chrome 114 / Safari 17 / Firefox 125; in an older engine the Delete button needs a one-line handler while the popover half keeps working.