09 / 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 Dropdown Matching Anchor Width
A panel exactly as wide as the control that opened it reads as part of that control. supports-[anchor-name:--a]:w-[anchor-size(width)] sizes the panel from its anchor live, with no offsetWidth — and because the variant is emitted after the base w-[min(23rem,…)], the fallback width is declared first for free. Resize the frame and the seam never opens.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tpop-09 transition-[--tpop09-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop09-d:1] [[data-theme=dark]_&]:[--tpop09-d:1] [[data-theme=light]_&]:[--tpop09-d:0] group/root w-full min-h-svh bg-tpop09-bg px-4.5 py-10 font-tpop09-sans text-tpop09-ink" aria-label="Dropdown matching anchor width demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<form class="w-full min-w-0 max-w-[25rem] rounded-xl border border-tpop09-rule bg-tpop09-surface px-4.5 pt-5 pb-4.5 shadow-[0_16px_36px_-28px_oklch(0.25_0.05_255/0.5)]" aria-label="Deployment target">
<p class="mb-4 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop09-brand">Deploy target</p>
<label for="tpop-09-trigger" class="mb-1.5 block text-[12.5px] font-semibold text-tpop09-mut">Environment</label>
<button id="tpop-09-trigger" type="button" popovertarget="tpop-09-list" class="flex min-h-11 w-full cursor-pointer items-center justify-between gap-2.5 rounded-lg border border-tpop09-rule bg-tpop09-surface px-3 text-left text-[14px] font-medium text-tpop09-ink transition-[border-color,border-radius,box-shadow] duration-150 [anchor-name:--tpop09-a] hover:border-tpop09-brand focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:border-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:shadow-[0_0_0_3px_oklch(0.5_0.15_255/0.14)] supports-[anchor-name:--a]:group-has-[#tpop-09-list:popover-open]/root:rounded-b-none">
<span class="flex min-w-0 items-center gap-2"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="truncate">Production · eu-west-1</span></span>
<svg class="flex-none text-tpop09-mut transition-transform duration-200 motion-reduce:transition-none group-has-[#tpop-09-list:popover-open]/root:rotate-180" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M4 6.5 8 10.5 12 6.5"/></svg>
</button>
<p class="mt-2 mb-3.5 text-[12.5px] text-tpop09-mut">The list below takes its width from this button, live. Resize the frame.</p>
<div class="flex flex-wrap gap-1.5">
<span class="rounded-md bg-tpop09-brand/10 px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-brand">w-[anchor-size(width)]</span>
<span class="rounded-md bg-tpop09-bg px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-mut">min-w variant in notes</span>
</div>
</form>
</div>
<div popover="auto" id="tpop-09-list" role="group" aria-label="Environments" class="fixed m-auto w-[min(23rem,calc(100vw-32px))] rounded-lg border border-tpop09-brand bg-tpop09-surface p-1.5 text-tpop09-ink opacity-0 shadow-[0_18px_36px_-20px_oklch(0.25_0.05_255/0.55)] transition-[opacity,translate,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop09-a] [position-area:block-end_span-inline-end] open:opacity-100 starting:open:-translate-y-1 starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:w-[anchor-size(width)] supports-[anchor-name:--a]:rounded-t-none supports-[anchor-name:--a]:border-t-0">
<p class="flex min-w-0 items-center gap-2.5 rounded-md bg-tpop09-brand/9 px-2.5 py-2.5 text-[13.5px] font-semibold text-tpop09-brand"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Production · eu-west-1</span><svg class="flex-none" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" aria-label="current"><path d="M3 8.5l3.2 3.2L13 4.8"/></svg></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Staging · eu-west-1</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-warn"></span><span class="min-w-0 flex-auto truncate">Preview · ephemeral per branch</span><span class="flex-none text-[11px] text-tpop09-mut">degraded</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-rule"></span><span class="min-w-0 flex-auto truncate">Local · docker compose</span><span class="flex-none text-[11px] text-tpop09-mut">offline</span></p>
</div>
</section><section class="tpop-09 transition-[--tpop09-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop09-d:1] [[data-theme=dark]_&]:[--tpop09-d:1] [[data-theme=light]_&]:[--tpop09-d:0] group/root w-full min-h-svh bg-tpop09-bg px-4.5 py-10 font-tpop09-sans text-tpop09-ink" aria-label="Dropdown matching anchor width demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<form class="w-full min-w-0 max-w-[25rem] rounded-xl border border-tpop09-rule bg-tpop09-surface px-4.5 pt-5 pb-4.5 shadow-[0_16px_36px_-28px_oklch(0.25_0.05_255/0.5)]" aria-label="Deployment target">
<p class="mb-4 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop09-brand">Deploy target</p>
<label for="tpop-09-trigger" class="mb-1.5 block text-[12.5px] font-semibold text-tpop09-mut">Environment</label>
<button id="tpop-09-trigger" type="button" popovertarget="tpop-09-list" class="flex min-h-11 w-full cursor-pointer items-center justify-between gap-2.5 rounded-lg border border-tpop09-rule bg-tpop09-surface px-3 text-left text-[14px] font-medium text-tpop09-ink transition-[border-color,border-radius,box-shadow] duration-150 [anchor-name:--tpop09-a] hover:border-tpop09-brand focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:border-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:shadow-[0_0_0_3px_oklch(0.5_0.15_255/0.14)] supports-[anchor-name:--a]:group-has-[#tpop-09-list:popover-open]/root:rounded-b-none">
<span class="flex min-w-0 items-center gap-2"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="truncate">Production · eu-west-1</span></span>
<svg class="flex-none text-tpop09-mut transition-transform duration-200 motion-reduce:transition-none group-has-[#tpop-09-list:popover-open]/root:rotate-180" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M4 6.5 8 10.5 12 6.5"/></svg>
</button>
<p class="mt-2 mb-3.5 text-[12.5px] text-tpop09-mut">The list below takes its width from this button, live. Resize the frame.</p>
<div class="flex flex-wrap gap-1.5">
<span class="rounded-md bg-tpop09-brand/10 px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-brand">w-[anchor-size(width)]</span>
<span class="rounded-md bg-tpop09-bg px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-mut">min-w variant in notes</span>
</div>
</form>
</div>
<div popover="auto" id="tpop-09-list" role="group" aria-label="Environments" class="fixed m-auto w-[min(23rem,calc(100vw-32px))] rounded-lg border border-tpop09-brand bg-tpop09-surface p-1.5 text-tpop09-ink opacity-0 shadow-[0_18px_36px_-20px_oklch(0.25_0.05_255/0.55)] transition-[opacity,translate,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop09-a] [position-area:block-end_span-inline-end] open:opacity-100 starting:open:-translate-y-1 starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:w-[anchor-size(width)] supports-[anchor-name:--a]:rounded-t-none supports-[anchor-name:--a]:border-t-0">
<p class="flex min-w-0 items-center gap-2.5 rounded-md bg-tpop09-brand/9 px-2.5 py-2.5 text-[13.5px] font-semibold text-tpop09-brand"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Production · eu-west-1</span><svg class="flex-none" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" aria-label="current"><path d="M3 8.5l3.2 3.2L13 4.8"/></svg></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Staging · eu-west-1</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-warn"></span><span class="min-w-0 flex-auto truncate">Preview · ephemeral per branch</span><span class="flex-none text-[11px] text-tpop09-mut">degraded</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-rule"></span><span class="min-w-0 flex-auto truncate">Local · docker compose</span><span class="flex-none text-[11px] text-tpop09-mut">offline</span></p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tpop09-bg: #f2f5f9;
--color-tpop09-bg: oklch(0.97 0.006 250);
--color-tpop09-surface: #ffffff;
--color-tpop09-surface: oklch(1 0 0);
--color-tpop09-ink: #111b2b;
--color-tpop09-ink: oklch(0.22 0.035 260);
--color-tpop09-mut: #515e75;
--color-tpop09-mut: oklch(0.48 0.04 260);
--color-tpop09-rule: #ccd5e2;
--color-tpop09-rule: oklch(0.87 0.02 255);
--color-tpop09-brand: #1558ad;
--color-tpop09-brand: oklch(0.47 0.15 257);
--color-tpop09-ok: #29ac68;
--color-tpop09-ok: oklch(0.66 0.15 155);
--color-tpop09-warn: #e8a127;
--color-tpop09-warn: oklch(0.76 0.15 75);
--font-tpop09-sans: 'Geist', system-ui, sans-serif;
--font-tpop09-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop09-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-09 {
width: 100%;
min-height: 100svh;
--color-tpop09-bg: color-mix(in oklch, oklch(0.16 0.018 258) calc(var(--tpop09-d) * 100%), oklch(0.97 0.006 250));
--color-tpop09-surface: color-mix(in oklch, oklch(0.21 0.022 258) calc(var(--tpop09-d) * 100%), oklch(1 0 0));
--color-tpop09-ink: color-mix(in oklch, oklch(0.95 0.01 255) calc(var(--tpop09-d) * 100%), oklch(0.22 0.035 260));
--color-tpop09-mut: color-mix(in oklch, oklch(0.73 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.48 0.04 260));
--color-tpop09-rule: color-mix(in oklch, oklch(0.32 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.87 0.02 255));
--color-tpop09-brand: color-mix(in oklch, oklch(0.72 0.13 257) calc(var(--tpop09-d) * 100%), oklch(0.47 0.15 257));
--color-tpop09-ok: color-mix(in oklch, oklch(0.74 0.15 155) calc(var(--tpop09-d) * 100%), oklch(0.66 0.15 155));
--color-tpop09-warn: color-mix(in oklch, oklch(0.8 0.14 75) calc(var(--tpop09-d) * 100%), oklch(0.76 0.15 75));
}@import "tailwindcss";
@theme {
--color-tpop09-bg: #f2f5f9;
--color-tpop09-bg: oklch(0.97 0.006 250);
--color-tpop09-surface: #ffffff;
--color-tpop09-surface: oklch(1 0 0);
--color-tpop09-ink: #111b2b;
--color-tpop09-ink: oklch(0.22 0.035 260);
--color-tpop09-mut: #515e75;
--color-tpop09-mut: oklch(0.48 0.04 260);
--color-tpop09-rule: #ccd5e2;
--color-tpop09-rule: oklch(0.87 0.02 255);
--color-tpop09-brand: #1558ad;
--color-tpop09-brand: oklch(0.47 0.15 257);
--color-tpop09-ok: #29ac68;
--color-tpop09-ok: oklch(0.66 0.15 155);
--color-tpop09-warn: #e8a127;
--color-tpop09-warn: oklch(0.76 0.15 75);
--font-tpop09-sans: 'Geist', system-ui, sans-serif;
--font-tpop09-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop09-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-09 {
width: 100%;
min-height: 100svh;
--color-tpop09-bg: color-mix(in oklch, oklch(0.16 0.018 258) calc(var(--tpop09-d) * 100%), oklch(0.97 0.006 250));
--color-tpop09-surface: color-mix(in oklch, oklch(0.21 0.022 258) calc(var(--tpop09-d) * 100%), oklch(1 0 0));
--color-tpop09-ink: color-mix(in oklch, oklch(0.95 0.01 255) calc(var(--tpop09-d) * 100%), oklch(0.22 0.035 260));
--color-tpop09-mut: color-mix(in oklch, oklch(0.73 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.48 0.04 260));
--color-tpop09-rule: color-mix(in oklch, oklch(0.32 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.87 0.02 255));
--color-tpop09-brand: color-mix(in oklch, oklch(0.72 0.13 257) calc(var(--tpop09-d) * 100%), oklch(0.47 0.15 257));
--color-tpop09-ok: color-mix(in oklch, oklch(0.74 0.15 155) calc(var(--tpop09-d) * 100%), oklch(0.66 0.15 155));
--color-tpop09-warn: color-mix(in oklch, oklch(0.8 0.14 75) calc(var(--tpop09-d) * 100%), oklch(0.76 0.15 75));
}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-tpop09-bg: #f2f5f9;
--color-tpop09-bg: oklch(0.97 0.006 250);
--color-tpop09-surface: #ffffff;
--color-tpop09-surface: oklch(1 0 0);
--color-tpop09-ink: #111b2b;
--color-tpop09-ink: oklch(0.22 0.035 260);
--color-tpop09-mut: #515e75;
--color-tpop09-mut: oklch(0.48 0.04 260);
--color-tpop09-rule: #ccd5e2;
--color-tpop09-rule: oklch(0.87 0.02 255);
--color-tpop09-brand: #1558ad;
--color-tpop09-brand: oklch(0.47 0.15 257);
--color-tpop09-ok: #29ac68;
--color-tpop09-ok: oklch(0.66 0.15 155);
--color-tpop09-warn: #e8a127;
--color-tpop09-warn: oklch(0.76 0.15 75);
--font-tpop09-sans: 'Geist', system-ui, sans-serif;
--font-tpop09-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop09-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-09 {
width: 100%;
min-height: 100svh;
--color-tpop09-bg: color-mix(in oklch, oklch(0.16 0.018 258) calc(var(--tpop09-d) * 100%), oklch(0.97 0.006 250));
--color-tpop09-surface: color-mix(in oklch, oklch(0.21 0.022 258) calc(var(--tpop09-d) * 100%), oklch(1 0 0));
--color-tpop09-ink: color-mix(in oklch, oklch(0.95 0.01 255) calc(var(--tpop09-d) * 100%), oklch(0.22 0.035 260));
--color-tpop09-mut: color-mix(in oklch, oklch(0.73 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.48 0.04 260));
--color-tpop09-rule: color-mix(in oklch, oklch(0.32 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.87 0.02 255));
--color-tpop09-brand: color-mix(in oklch, oklch(0.72 0.13 257) calc(var(--tpop09-d) * 100%), oklch(0.47 0.15 257));
--color-tpop09-ok: color-mix(in oklch, oklch(0.74 0.15 155) calc(var(--tpop09-d) * 100%), oklch(0.66 0.15 155));
--color-tpop09-warn: color-mix(in oklch, oklch(0.8 0.14 75) calc(var(--tpop09-d) * 100%), oklch(0.76 0.15 75));
}@import "tailwindcss";
@theme {
--color-tpop09-bg: #f2f5f9;
--color-tpop09-bg: oklch(0.97 0.006 250);
--color-tpop09-surface: #ffffff;
--color-tpop09-surface: oklch(1 0 0);
--color-tpop09-ink: #111b2b;
--color-tpop09-ink: oklch(0.22 0.035 260);
--color-tpop09-mut: #515e75;
--color-tpop09-mut: oklch(0.48 0.04 260);
--color-tpop09-rule: #ccd5e2;
--color-tpop09-rule: oklch(0.87 0.02 255);
--color-tpop09-brand: #1558ad;
--color-tpop09-brand: oklch(0.47 0.15 257);
--color-tpop09-ok: #29ac68;
--color-tpop09-ok: oklch(0.66 0.15 155);
--color-tpop09-warn: #e8a127;
--color-tpop09-warn: oklch(0.76 0.15 75);
--font-tpop09-sans: 'Geist', system-ui, sans-serif;
--font-tpop09-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop09-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-09 {
width: 100%;
min-height: 100svh;
--color-tpop09-bg: color-mix(in oklch, oklch(0.16 0.018 258) calc(var(--tpop09-d) * 100%), oklch(0.97 0.006 250));
--color-tpop09-surface: color-mix(in oklch, oklch(0.21 0.022 258) calc(var(--tpop09-d) * 100%), oklch(1 0 0));
--color-tpop09-ink: color-mix(in oklch, oklch(0.95 0.01 255) calc(var(--tpop09-d) * 100%), oklch(0.22 0.035 260));
--color-tpop09-mut: color-mix(in oklch, oklch(0.73 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.48 0.04 260));
--color-tpop09-rule: color-mix(in oklch, oklch(0.32 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.87 0.02 255));
--color-tpop09-brand: color-mix(in oklch, oklch(0.72 0.13 257) calc(var(--tpop09-d) * 100%), oklch(0.47 0.15 257));
--color-tpop09-ok: color-mix(in oklch, oklch(0.74 0.15 155) calc(var(--tpop09-d) * 100%), oklch(0.66 0.15 155));
--color-tpop09-warn: color-mix(in oklch, oklch(0.8 0.14 75) calc(var(--tpop09-d) * 100%), oklch(0.76 0.15 75));
}<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">
<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 fixed-width rule and its
@supportsoverride became two width utilities on one element — basew-[min(…)], thensupports-[anchor-name:--a]:w-[anchor-size(width)]— and Tailwind's emit order guarantees the fallback comes first. .pop-09__opt + .pop-09__optandoverflow/text-overflow/white-spacebecametruncateon the label span.- UI upgrade from the CSS original: field and list fuse into one shape while open — the field loses its bottom radius, the list its top border and radius — gated so the seam only closes where anchoring actually tethers them.
- UI upgrade: a focus-style halo and rotating chevron on the open field, environment health dots paired with text labels (degraded, offline), a checkmark on the current option, and the list now drops in from the field.
- UI upgrade — dark mode: the stage now follows
prefers-color-scheme(and adata-themeoverride on any ancestor) through one registered dial,--tpop09-d. The root rule re-declares each@themecolour ascolor-mix(dark calc(var(--tpop09-d)*100%), light), the root sets the dial withdark:[--tpop09-d:1], andtransition-[--tpop09-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 Dropdown Matching Anchor Width
Source: https://codefronts.com/snippets/tailwind-css-popovers/tailwind-dropdown-matching-anchor-width/
A panel exactly as wide as the control that opened it reads as part of that control. supports-[anchor-name:--a]:w-[anchor-size(width)] sizes the panel from its anchor live, with no offsetWidth — and because the variant is emitted after the base w-[min(23rem,…)], the fallback width is declared first for free. Resize the frame and the seam never opens.
## HTML
```html
<section class="tpop-09 transition-[--tpop09-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop09-d:1] [[data-theme=dark]_&]:[--tpop09-d:1] [[data-theme=light]_&]:[--tpop09-d:0] group/root w-full min-h-svh bg-tpop09-bg px-4.5 py-10 font-tpop09-sans text-tpop09-ink" aria-label="Dropdown matching anchor width demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<form class="w-full min-w-0 max-w-[25rem] rounded-xl border border-tpop09-rule bg-tpop09-surface px-4.5 pt-5 pb-4.5 shadow-[0_16px_36px_-28px_oklch(0.25_0.05_255/0.5)]" aria-label="Deployment target">
<p class="mb-4 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop09-brand">Deploy target</p>
<label for="tpop-09-trigger" class="mb-1.5 block text-[12.5px] font-semibold text-tpop09-mut">Environment</label>
<button id="tpop-09-trigger" type="button" popovertarget="tpop-09-list" class="flex min-h-11 w-full cursor-pointer items-center justify-between gap-2.5 rounded-lg border border-tpop09-rule bg-tpop09-surface px-3 text-left text-[14px] font-medium text-tpop09-ink transition-[border-color,border-radius,box-shadow] duration-150 [anchor-name:--tpop09-a] hover:border-tpop09-brand focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:border-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:shadow-[0_0_0_3px_oklch(0.5_0.15_255/0.14)] supports-[anchor-name:--a]:group-has-[#tpop-09-list:popover-open]/root:rounded-b-none">
<span class="flex min-w-0 items-center gap-2"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="truncate">Production · eu-west-1</span></span>
<svg class="flex-none text-tpop09-mut transition-transform duration-200 motion-reduce:transition-none group-has-[#tpop-09-list:popover-open]/root:rotate-180" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M4 6.5 8 10.5 12 6.5"/></svg>
</button>
<p class="mt-2 mb-3.5 text-[12.5px] text-tpop09-mut">The list below takes its width from this button, live. Resize the frame.</p>
<div class="flex flex-wrap gap-1.5">
<span class="rounded-md bg-tpop09-brand/10 px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-brand">w-[anchor-size(width)]</span>
<span class="rounded-md bg-tpop09-bg px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-mut">min-w variant in notes</span>
</div>
</form>
</div>
<div popover="auto" id="tpop-09-list" role="group" aria-label="Environments" class="fixed m-auto w-[min(23rem,calc(100vw-32px))] rounded-lg border border-tpop09-brand bg-tpop09-surface p-1.5 text-tpop09-ink opacity-0 shadow-[0_18px_36px_-20px_oklch(0.25_0.05_255/0.55)] transition-[opacity,translate,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop09-a] [position-area:block-end_span-inline-end] open:opacity-100 starting:open:-translate-y-1 starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:w-[anchor-size(width)] supports-[anchor-name:--a]:rounded-t-none supports-[anchor-name:--a]:border-t-0">
<p class="flex min-w-0 items-center gap-2.5 rounded-md bg-tpop09-brand/9 px-2.5 py-2.5 text-[13.5px] font-semibold text-tpop09-brand"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Production · eu-west-1</span><svg class="flex-none" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" aria-label="current"><path d="M3 8.5l3.2 3.2L13 4.8"/></svg></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Staging · eu-west-1</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-warn"></span><span class="min-w-0 flex-auto truncate">Preview · ephemeral per branch</span><span class="flex-none text-[11px] text-tpop09-mut">degraded</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-rule"></span><span class="min-w-0 flex-auto truncate">Local · docker compose</span><span class="flex-none text-[11px] text-tpop09-mut">offline</span></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpop09-bg: #f2f5f9;
--color-tpop09-bg: oklch(0.97 0.006 250);
--color-tpop09-surface: #ffffff;
--color-tpop09-surface: oklch(1 0 0);
--color-tpop09-ink: #111b2b;
--color-tpop09-ink: oklch(0.22 0.035 260);
--color-tpop09-mut: #515e75;
--color-tpop09-mut: oklch(0.48 0.04 260);
--color-tpop09-rule: #ccd5e2;
--color-tpop09-rule: oklch(0.87 0.02 255);
--color-tpop09-brand: #1558ad;
--color-tpop09-brand: oklch(0.47 0.15 257);
--color-tpop09-ok: #29ac68;
--color-tpop09-ok: oklch(0.66 0.15 155);
--color-tpop09-warn: #e8a127;
--color-tpop09-warn: oklch(0.76 0.15 75);
--font-tpop09-sans: 'Geist', system-ui, sans-serif;
--font-tpop09-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop09-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-09 {
width: 100%;
min-height: 100svh;
--color-tpop09-bg: color-mix(in oklch, oklch(0.16 0.018 258) calc(var(--tpop09-d) * 100%), oklch(0.97 0.006 250));
--color-tpop09-surface: color-mix(in oklch, oklch(0.21 0.022 258) calc(var(--tpop09-d) * 100%), oklch(1 0 0));
--color-tpop09-ink: color-mix(in oklch, oklch(0.95 0.01 255) calc(var(--tpop09-d) * 100%), oklch(0.22 0.035 260));
--color-tpop09-mut: color-mix(in oklch, oklch(0.73 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.48 0.04 260));
--color-tpop09-rule: color-mix(in oklch, oklch(0.32 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.87 0.02 255));
--color-tpop09-brand: color-mix(in oklch, oklch(0.72 0.13 257) calc(var(--tpop09-d) * 100%), oklch(0.47 0.15 257));
--color-tpop09-ok: color-mix(in oklch, oklch(0.74 0.15 155) calc(var(--tpop09-d) * 100%), oklch(0.66 0.15 155));
--color-tpop09-warn: color-mix(in oklch, oklch(0.8 0.14 75) calc(var(--tpop09-d) * 100%), oklch(0.76 0.15 75));
}
```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 Dropdown Matching Anchor Width
Source: https://codefronts.com/snippets/tailwind-css-popovers/tailwind-dropdown-matching-anchor-width/
A panel exactly as wide as the control that opened it reads as part of that control. supports-[anchor-name:--a]:w-[anchor-size(width)] sizes the panel from its anchor live, with no offsetWidth — and because the variant is emitted after the base w-[min(23rem,…)], the fallback width is declared first for free. Resize the frame and the seam never opens.
## HTML
```html
<section class="tpop-09 transition-[--tpop09-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop09-d:1] [[data-theme=dark]_&]:[--tpop09-d:1] [[data-theme=light]_&]:[--tpop09-d:0] group/root w-full min-h-svh bg-tpop09-bg px-4.5 py-10 font-tpop09-sans text-tpop09-ink" aria-label="Dropdown matching anchor width demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<form class="w-full min-w-0 max-w-[25rem] rounded-xl border border-tpop09-rule bg-tpop09-surface px-4.5 pt-5 pb-4.5 shadow-[0_16px_36px_-28px_oklch(0.25_0.05_255/0.5)]" aria-label="Deployment target">
<p class="mb-4 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop09-brand">Deploy target</p>
<label for="tpop-09-trigger" class="mb-1.5 block text-[12.5px] font-semibold text-tpop09-mut">Environment</label>
<button id="tpop-09-trigger" type="button" popovertarget="tpop-09-list" class="flex min-h-11 w-full cursor-pointer items-center justify-between gap-2.5 rounded-lg border border-tpop09-rule bg-tpop09-surface px-3 text-left text-[14px] font-medium text-tpop09-ink transition-[border-color,border-radius,box-shadow] duration-150 [anchor-name:--tpop09-a] hover:border-tpop09-brand focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:border-tpop09-brand group-has-[#tpop-09-list:popover-open]/root:shadow-[0_0_0_3px_oklch(0.5_0.15_255/0.14)] supports-[anchor-name:--a]:group-has-[#tpop-09-list:popover-open]/root:rounded-b-none">
<span class="flex min-w-0 items-center gap-2"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="truncate">Production · eu-west-1</span></span>
<svg class="flex-none text-tpop09-mut transition-transform duration-200 motion-reduce:transition-none group-has-[#tpop-09-list:popover-open]/root:rotate-180" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M4 6.5 8 10.5 12 6.5"/></svg>
</button>
<p class="mt-2 mb-3.5 text-[12.5px] text-tpop09-mut">The list below takes its width from this button, live. Resize the frame.</p>
<div class="flex flex-wrap gap-1.5">
<span class="rounded-md bg-tpop09-brand/10 px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-brand">w-[anchor-size(width)]</span>
<span class="rounded-md bg-tpop09-bg px-2 py-1.5 font-tpop09-mono text-[11px] font-semibold leading-tight text-tpop09-mut">min-w variant in notes</span>
</div>
</form>
</div>
<div popover="auto" id="tpop-09-list" role="group" aria-label="Environments" class="fixed m-auto w-[min(23rem,calc(100vw-32px))] rounded-lg border border-tpop09-brand bg-tpop09-surface p-1.5 text-tpop09-ink opacity-0 shadow-[0_18px_36px_-20px_oklch(0.25_0.05_255/0.55)] transition-[opacity,translate,display,overlay] duration-150 ease-out transition-discrete [position-anchor:--tpop09-a] [position-area:block-end_span-inline-end] open:opacity-100 starting:open:-translate-y-1 starting:open:opacity-0 motion-reduce:transition-none supports-[anchor-name:--a]:inset-auto supports-[anchor-name:--a]:m-0 supports-[anchor-name:--a]:w-[anchor-size(width)] supports-[anchor-name:--a]:rounded-t-none supports-[anchor-name:--a]:border-t-0">
<p class="flex min-w-0 items-center gap-2.5 rounded-md bg-tpop09-brand/9 px-2.5 py-2.5 text-[13.5px] font-semibold text-tpop09-brand"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Production · eu-west-1</span><svg class="flex-none" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" aria-label="current"><path d="M3 8.5l3.2 3.2L13 4.8"/></svg></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-ok"></span><span class="min-w-0 flex-auto truncate">Staging · eu-west-1</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-warn"></span><span class="min-w-0 flex-auto truncate">Preview · ephemeral per branch</span><span class="flex-none text-[11px] text-tpop09-mut">degraded</span></p>
<p class="flex min-w-0 items-center gap-2.5 rounded-md px-2.5 py-2.5 text-[13.5px]"><span aria-hidden="true" class="size-2 flex-none rounded-full bg-tpop09-rule"></span><span class="min-w-0 flex-auto truncate">Local · docker compose</span><span class="flex-none text-[11px] text-tpop09-mut">offline</span></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpop09-bg: #f2f5f9;
--color-tpop09-bg: oklch(0.97 0.006 250);
--color-tpop09-surface: #ffffff;
--color-tpop09-surface: oklch(1 0 0);
--color-tpop09-ink: #111b2b;
--color-tpop09-ink: oklch(0.22 0.035 260);
--color-tpop09-mut: #515e75;
--color-tpop09-mut: oklch(0.48 0.04 260);
--color-tpop09-rule: #ccd5e2;
--color-tpop09-rule: oklch(0.87 0.02 255);
--color-tpop09-brand: #1558ad;
--color-tpop09-brand: oklch(0.47 0.15 257);
--color-tpop09-ok: #29ac68;
--color-tpop09-ok: oklch(0.66 0.15 155);
--color-tpop09-warn: #e8a127;
--color-tpop09-warn: oklch(0.76 0.15 75);
--font-tpop09-sans: 'Geist', system-ui, sans-serif;
--font-tpop09-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop09-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-09 {
width: 100%;
min-height: 100svh;
--color-tpop09-bg: color-mix(in oklch, oklch(0.16 0.018 258) calc(var(--tpop09-d) * 100%), oklch(0.97 0.006 250));
--color-tpop09-surface: color-mix(in oklch, oklch(0.21 0.022 258) calc(var(--tpop09-d) * 100%), oklch(1 0 0));
--color-tpop09-ink: color-mix(in oklch, oklch(0.95 0.01 255) calc(var(--tpop09-d) * 100%), oklch(0.22 0.035 260));
--color-tpop09-mut: color-mix(in oklch, oklch(0.73 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.48 0.04 260));
--color-tpop09-rule: color-mix(in oklch, oklch(0.32 0.03 258) calc(var(--tpop09-d) * 100%), oklch(0.87 0.02 255));
--color-tpop09-brand: color-mix(in oklch, oklch(0.72 0.13 257) calc(var(--tpop09-d) * 100%), oklch(0.47 0.15 257));
--color-tpop09-ok: color-mix(in oklch, oklch(0.74 0.15 155) calc(var(--tpop09-d) * 100%), oklch(0.66 0.15 155));
--color-tpop09-warn: color-mix(in oklch, oklch(0.8 0.14 75) calc(var(--tpop09-d) * 100%), oklch(0.76 0.15 75));
}
```How this works
anchor-size() turns the anchor's box into a length.
<button class="w-full [anchor-name:--tpop09-a]" popovertarget="tpop-09-list">
<div popover id="tpop-09-list" class="fixed m-auto
w-[min(23rem,calc(100vw-32px))]
supports-[anchor-name:--a]:w-[anchor-size(width)]
[position-anchor:--tpop09-a]
[position-area:block-end_span-inline-end]">Order is the fallback. The CSS version had to declare the fixed width before the gated one by hand. In Tailwind, variant utilities are always emitted after base utilities, so the unanchored width is the floor and anchor-size(width) overrides it wherever it's supported.
Fusing the seam. While the list is open the field drops its bottom radius and the list drops its top radius and top border (rounded-t-none border-t-0) — the two boxes become one shape, the detail that separates a dropdown from a floating box.
Name the right element. anchor-size() reads whatever position-anchor names; put anchor-name on the button, not its chevron.
Make it yours
- Grow-only: use
supports-[anchor-name:--a]:min-w-[anchor-size(width)]andw-maxso long labels widen the panel. - Overhang:
supports-[anchor-name:--a]:w-[calc(anchor-size(width)+1.5rem)]with[justify-self:anchor-center]. - Open upward:
[position-area:block-start_span-inline-end]and swap the fused radii to the other edge.
Gotchas — read before shipping
- Naming the chevron instead of the button gives you the chevron's width.
- An exact width truncates long labels — keep
truncateon options, or use the min-width variant. - A trigger that changes width on hover resizes the open panel mid-interaction; keep its box stable.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 125+ | 26+ | 131+ | 125+ |
anchor-size() shares anchor positioning's floor. A popover-only engine opens the list centred at 23rem, and the fused-seam styling stays harmless.