08 / 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 Tooltip Arrow Pointing to Anchor
The bubble is easy; the tail is what breaks. Here the tail is the bubble's after: pseudo-element, itself anchor-positioned to the trigger with the same area and the same flip list, so it stays on the button's midline when the bubble is clamped or flipped. Every rule of it is an after: utility — gated so unanchored engines draw no tail rather than a wrong one.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tpop-08 transition-[--tpop08-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop08-d:1] [[data-theme=dark]_&]:[--tpop08-d:1] [[data-theme=light]_&]:[--tpop08-d:0] group/root w-full min-h-svh bg-tpop08-bg px-4.5 py-10 font-tpop08-sans text-tpop08-ink" aria-label="Tooltip arrow pointing to anchor demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[27rem] rounded-2xl border border-tpop08-rule bg-tpop08-surface px-4.5 pt-5 pb-4.5 shadow-[0_14px_30px_-22px_oklch(0.3_0.03_150/0.45)]">
<p class="mb-3.5 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop08-sage">Team roster</p>
<ul class="grid">
<li class="flex min-w-0 items-center gap-3 border-b border-tpop08-rule py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-sage/14 text-[11px] font-bold text-tpop08-sage">PS</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Priya Sharma</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Reviewer</span>
<button id="tpop-08-trigger" type="button" popovertarget="tpop-08-tip" aria-label="What reviewer permissions mean" aria-describedby="tpop-08-tip" class="inline-flex size-11 flex-none cursor-pointer items-center justify-center rounded-full border border-tpop08-rule text-tpop08-sage transition-colors [anchor-name:--tpop08-a] hover:bg-tpop08-bg focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:border-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:bg-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:text-tpop08-surface">
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="10" cy="10" r="7.2"/><path d="M10 9v5M10 6.3v.8"/></svg>
</button>
</li>
<li class="flex min-w-0 items-center gap-3 py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-ink/8 text-[11px] font-bold text-tpop08-ink">SR</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Sam Rivera</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Owner</span>
<span aria-hidden="true" class="w-11 flex-none"></span>
</li>
</ul>
<p class="mt-4 text-[12.5px] leading-relaxed text-tpop08-mut">Open the hint, then narrow the frame — the tail follows the button, not the bubble.</p>
</div>
</div>
<div popover="auto" id="tpop-08-tip" role="tooltip" class="fixed m-auto w-[min(15rem,calc(100vw-28px))] rounded-2xl bg-tpop08-ink px-3.5 py-2.5 text-[13px] leading-normal text-tpop08-surface opacity-0 shadow-[0_12px_24px_-10px_oklch(0.3_0.03_150/0.45)] text-pretty transition-[opacity,display,overlay] duration-150 transition-discrete [position-anchor:--tpop08-a] [position-area:block-end_center] [justify-self:anchor-center] [position-try-fallbacks:flip-block] open:opacity-100 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-3 after:size-3 after:rotate-45 after:rounded-[3px] after:bg-tpop08-ink after:[position-anchor:--tpop08-a] after:[position-area:block-end_center] after:[justify-self:anchor-center] after:[position-try-fallbacks:flip-block] supports-[anchor-name:--a]:after:fixed supports-[anchor-name:--a]:after:inset-auto supports-[anchor-name:--a]:after:m-0 supports-[anchor-name:--a]:after:mt-1.5 supports-[anchor-name:--a]:after:content-['']">
Reviewers can comment and approve, but cannot merge or change settings.
</div>
</section><section class="tpop-08 transition-[--tpop08-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop08-d:1] [[data-theme=dark]_&]:[--tpop08-d:1] [[data-theme=light]_&]:[--tpop08-d:0] group/root w-full min-h-svh bg-tpop08-bg px-4.5 py-10 font-tpop08-sans text-tpop08-ink" aria-label="Tooltip arrow pointing to anchor demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[27rem] rounded-2xl border border-tpop08-rule bg-tpop08-surface px-4.5 pt-5 pb-4.5 shadow-[0_14px_30px_-22px_oklch(0.3_0.03_150/0.45)]">
<p class="mb-3.5 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop08-sage">Team roster</p>
<ul class="grid">
<li class="flex min-w-0 items-center gap-3 border-b border-tpop08-rule py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-sage/14 text-[11px] font-bold text-tpop08-sage">PS</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Priya Sharma</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Reviewer</span>
<button id="tpop-08-trigger" type="button" popovertarget="tpop-08-tip" aria-label="What reviewer permissions mean" aria-describedby="tpop-08-tip" class="inline-flex size-11 flex-none cursor-pointer items-center justify-center rounded-full border border-tpop08-rule text-tpop08-sage transition-colors [anchor-name:--tpop08-a] hover:bg-tpop08-bg focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:border-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:bg-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:text-tpop08-surface">
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="10" cy="10" r="7.2"/><path d="M10 9v5M10 6.3v.8"/></svg>
</button>
</li>
<li class="flex min-w-0 items-center gap-3 py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-ink/8 text-[11px] font-bold text-tpop08-ink">SR</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Sam Rivera</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Owner</span>
<span aria-hidden="true" class="w-11 flex-none"></span>
</li>
</ul>
<p class="mt-4 text-[12.5px] leading-relaxed text-tpop08-mut">Open the hint, then narrow the frame — the tail follows the button, not the bubble.</p>
</div>
</div>
<div popover="auto" id="tpop-08-tip" role="tooltip" class="fixed m-auto w-[min(15rem,calc(100vw-28px))] rounded-2xl bg-tpop08-ink px-3.5 py-2.5 text-[13px] leading-normal text-tpop08-surface opacity-0 shadow-[0_12px_24px_-10px_oklch(0.3_0.03_150/0.45)] text-pretty transition-[opacity,display,overlay] duration-150 transition-discrete [position-anchor:--tpop08-a] [position-area:block-end_center] [justify-self:anchor-center] [position-try-fallbacks:flip-block] open:opacity-100 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-3 after:size-3 after:rotate-45 after:rounded-[3px] after:bg-tpop08-ink after:[position-anchor:--tpop08-a] after:[position-area:block-end_center] after:[justify-self:anchor-center] after:[position-try-fallbacks:flip-block] supports-[anchor-name:--a]:after:fixed supports-[anchor-name:--a]:after:inset-auto supports-[anchor-name:--a]:after:m-0 supports-[anchor-name:--a]:after:mt-1.5 supports-[anchor-name:--a]:after:content-['']">
Reviewers can comment and approve, but cannot merge or change settings.
</div>
</section>@import "tailwindcss";
@theme {
--color-tpop08-bg: #ecf0e8;
--color-tpop08-bg: oklch(0.95 0.012 130);
--color-tpop08-surface: #ffffff;
--color-tpop08-surface: oklch(1 0 0);
--color-tpop08-ink: #18281e;
--color-tpop08-ink: oklch(0.26 0.03 155);
--color-tpop08-mut: #506353;
--color-tpop08-mut: oklch(0.48 0.035 150);
--color-tpop08-rule: #dae0d7;
--color-tpop08-rule: oklch(0.9 0.015 135);
--color-tpop08-sage: #386649;
--color-tpop08-sage: oklch(0.47 0.07 155);
--font-tpop08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop08-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-08 {
width: 100%;
min-height: 100svh;
--color-tpop08-bg: color-mix(in oklch, oklch(0.17 0.012 150) calc(var(--tpop08-d) * 100%), oklch(0.95 0.012 130));
--color-tpop08-surface: color-mix(in oklch, oklch(0.215 0.014 150) calc(var(--tpop08-d) * 100%), oklch(1 0 0));
--color-tpop08-ink: color-mix(in oklch, oklch(0.94 0.012 140) calc(var(--tpop08-d) * 100%), oklch(0.26 0.03 155));
--color-tpop08-mut: color-mix(in oklch, oklch(0.73 0.03 145) calc(var(--tpop08-d) * 100%), oklch(0.48 0.035 150));
--color-tpop08-rule: color-mix(in oklch, oklch(0.31 0.016 150) calc(var(--tpop08-d) * 100%), oklch(0.9 0.015 135));
--color-tpop08-sage: color-mix(in oklch, oklch(0.76 0.09 155) calc(var(--tpop08-d) * 100%), oklch(0.47 0.07 155));
}@import "tailwindcss";
@theme {
--color-tpop08-bg: #ecf0e8;
--color-tpop08-bg: oklch(0.95 0.012 130);
--color-tpop08-surface: #ffffff;
--color-tpop08-surface: oklch(1 0 0);
--color-tpop08-ink: #18281e;
--color-tpop08-ink: oklch(0.26 0.03 155);
--color-tpop08-mut: #506353;
--color-tpop08-mut: oklch(0.48 0.035 150);
--color-tpop08-rule: #dae0d7;
--color-tpop08-rule: oklch(0.9 0.015 135);
--color-tpop08-sage: #386649;
--color-tpop08-sage: oklch(0.47 0.07 155);
--font-tpop08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop08-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-08 {
width: 100%;
min-height: 100svh;
--color-tpop08-bg: color-mix(in oklch, oklch(0.17 0.012 150) calc(var(--tpop08-d) * 100%), oklch(0.95 0.012 130));
--color-tpop08-surface: color-mix(in oklch, oklch(0.215 0.014 150) calc(var(--tpop08-d) * 100%), oklch(1 0 0));
--color-tpop08-ink: color-mix(in oklch, oklch(0.94 0.012 140) calc(var(--tpop08-d) * 100%), oklch(0.26 0.03 155));
--color-tpop08-mut: color-mix(in oklch, oklch(0.73 0.03 145) calc(var(--tpop08-d) * 100%), oklch(0.48 0.035 150));
--color-tpop08-rule: color-mix(in oklch, oklch(0.31 0.016 150) calc(var(--tpop08-d) * 100%), oklch(0.9 0.015 135));
--color-tpop08-sage: color-mix(in oklch, oklch(0.76 0.09 155) calc(var(--tpop08-d) * 100%), oklch(0.47 0.07 155));
}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-tpop08-bg: #ecf0e8;
--color-tpop08-bg: oklch(0.95 0.012 130);
--color-tpop08-surface: #ffffff;
--color-tpop08-surface: oklch(1 0 0);
--color-tpop08-ink: #18281e;
--color-tpop08-ink: oklch(0.26 0.03 155);
--color-tpop08-mut: #506353;
--color-tpop08-mut: oklch(0.48 0.035 150);
--color-tpop08-rule: #dae0d7;
--color-tpop08-rule: oklch(0.9 0.015 135);
--color-tpop08-sage: #386649;
--color-tpop08-sage: oklch(0.47 0.07 155);
--font-tpop08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop08-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-08 {
width: 100%;
min-height: 100svh;
--color-tpop08-bg: color-mix(in oklch, oklch(0.17 0.012 150) calc(var(--tpop08-d) * 100%), oklch(0.95 0.012 130));
--color-tpop08-surface: color-mix(in oklch, oklch(0.215 0.014 150) calc(var(--tpop08-d) * 100%), oklch(1 0 0));
--color-tpop08-ink: color-mix(in oklch, oklch(0.94 0.012 140) calc(var(--tpop08-d) * 100%), oklch(0.26 0.03 155));
--color-tpop08-mut: color-mix(in oklch, oklch(0.73 0.03 145) calc(var(--tpop08-d) * 100%), oklch(0.48 0.035 150));
--color-tpop08-rule: color-mix(in oklch, oklch(0.31 0.016 150) calc(var(--tpop08-d) * 100%), oklch(0.9 0.015 135));
--color-tpop08-sage: color-mix(in oklch, oklch(0.76 0.09 155) calc(var(--tpop08-d) * 100%), oklch(0.47 0.07 155));
}@import "tailwindcss";
@theme {
--color-tpop08-bg: #ecf0e8;
--color-tpop08-bg: oklch(0.95 0.012 130);
--color-tpop08-surface: #ffffff;
--color-tpop08-surface: oklch(1 0 0);
--color-tpop08-ink: #18281e;
--color-tpop08-ink: oklch(0.26 0.03 155);
--color-tpop08-mut: #506353;
--color-tpop08-mut: oklch(0.48 0.035 150);
--color-tpop08-rule: #dae0d7;
--color-tpop08-rule: oklch(0.9 0.015 135);
--color-tpop08-sage: #386649;
--color-tpop08-sage: oklch(0.47 0.07 155);
--font-tpop08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop08-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-08 {
width: 100%;
min-height: 100svh;
--color-tpop08-bg: color-mix(in oklch, oklch(0.17 0.012 150) calc(var(--tpop08-d) * 100%), oklch(0.95 0.012 130));
--color-tpop08-surface: color-mix(in oklch, oklch(0.215 0.014 150) calc(var(--tpop08-d) * 100%), oklch(1 0 0));
--color-tpop08-ink: color-mix(in oklch, oklch(0.94 0.012 140) calc(var(--tpop08-d) * 100%), oklch(0.26 0.03 155));
--color-tpop08-mut: color-mix(in oklch, oklch(0.73 0.03 145) calc(var(--tpop08-d) * 100%), oklch(0.48 0.035 150));
--color-tpop08-rule: color-mix(in oklch, oklch(0.31 0.016 150) calc(var(--tpop08-d) * 100%), oklch(0.9 0.015 135));
--color-tpop08-sage: color-mix(in oklch, oklch(0.76 0.09 155) calc(var(--tpop08-d) * 100%), oklch(0.47 0.07 155));
}<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=Plus+Jakarta+Sans:wght@400;500;600;700;800&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=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The
::afterrule and its@supports nottwin collapsed intoafter:utilities, with onlycontent,positionand the reset behindsupports-[anchor-name:--a]:— no anchoring means no generated box at all. justify-self: anchor-centerhas no named utility, so it's an arbitrary property on both the bubble and its tail.- Bug fix from the CSS original: the original used
filter: drop-shadow()on the bubble, which makes it the containing block for its fixed tail; the port uses abox-shadowand an opacity-only fade so the tail keeps resolving against the anchor. - UI upgrade: the two-line bubble moved from a pill (
999px, which pinches multi-line text) torounded-2xl; the info button fills while its hint is open; rows gained initials avatars and role chips; muted copy rose from#5f7263tooklch(0.48). - UI upgrade — dark mode: the stage now follows
prefers-color-scheme(and adata-themeoverride on any ancestor) through one registered dial,--tpop08-d. The root rule re-declares each@themecolour ascolor-mix(dark calc(var(--tpop08-d)*100%), light), the root sets the dial withdark:[--tpop08-d:1], andtransition-[--tpop08-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 Tooltip Arrow Pointing to Anchor
Source: https://codefronts.com/snippets/tailwind-css-popovers/tailwind-tooltip-arrow-pointing-to-anchor/
The bubble is easy; the tail is what breaks. Here the tail is the bubble's after: pseudo-element, itself anchor-positioned to the trigger with the same area and the same flip list, so it stays on the button's midline when the bubble is clamped or flipped. Every rule of it is an after: utility — gated so unanchored engines draw no tail rather than a wrong one.
## HTML
```html
<section class="tpop-08 transition-[--tpop08-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop08-d:1] [[data-theme=dark]_&]:[--tpop08-d:1] [[data-theme=light]_&]:[--tpop08-d:0] group/root w-full min-h-svh bg-tpop08-bg px-4.5 py-10 font-tpop08-sans text-tpop08-ink" aria-label="Tooltip arrow pointing to anchor demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[27rem] rounded-2xl border border-tpop08-rule bg-tpop08-surface px-4.5 pt-5 pb-4.5 shadow-[0_14px_30px_-22px_oklch(0.3_0.03_150/0.45)]">
<p class="mb-3.5 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop08-sage">Team roster</p>
<ul class="grid">
<li class="flex min-w-0 items-center gap-3 border-b border-tpop08-rule py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-sage/14 text-[11px] font-bold text-tpop08-sage">PS</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Priya Sharma</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Reviewer</span>
<button id="tpop-08-trigger" type="button" popovertarget="tpop-08-tip" aria-label="What reviewer permissions mean" aria-describedby="tpop-08-tip" class="inline-flex size-11 flex-none cursor-pointer items-center justify-center rounded-full border border-tpop08-rule text-tpop08-sage transition-colors [anchor-name:--tpop08-a] hover:bg-tpop08-bg focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:border-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:bg-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:text-tpop08-surface">
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="10" cy="10" r="7.2"/><path d="M10 9v5M10 6.3v.8"/></svg>
</button>
</li>
<li class="flex min-w-0 items-center gap-3 py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-ink/8 text-[11px] font-bold text-tpop08-ink">SR</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Sam Rivera</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Owner</span>
<span aria-hidden="true" class="w-11 flex-none"></span>
</li>
</ul>
<p class="mt-4 text-[12.5px] leading-relaxed text-tpop08-mut">Open the hint, then narrow the frame — the tail follows the button, not the bubble.</p>
</div>
</div>
<div popover="auto" id="tpop-08-tip" role="tooltip" class="fixed m-auto w-[min(15rem,calc(100vw-28px))] rounded-2xl bg-tpop08-ink px-3.5 py-2.5 text-[13px] leading-normal text-tpop08-surface opacity-0 shadow-[0_12px_24px_-10px_oklch(0.3_0.03_150/0.45)] text-pretty transition-[opacity,display,overlay] duration-150 transition-discrete [position-anchor:--tpop08-a] [position-area:block-end_center] [justify-self:anchor-center] [position-try-fallbacks:flip-block] open:opacity-100 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-3 after:size-3 after:rotate-45 after:rounded-[3px] after:bg-tpop08-ink after:[position-anchor:--tpop08-a] after:[position-area:block-end_center] after:[justify-self:anchor-center] after:[position-try-fallbacks:flip-block] supports-[anchor-name:--a]:after:fixed supports-[anchor-name:--a]:after:inset-auto supports-[anchor-name:--a]:after:m-0 supports-[anchor-name:--a]:after:mt-1.5 supports-[anchor-name:--a]:after:content-['']">
Reviewers can comment and approve, but cannot merge or change settings.
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpop08-bg: #ecf0e8;
--color-tpop08-bg: oklch(0.95 0.012 130);
--color-tpop08-surface: #ffffff;
--color-tpop08-surface: oklch(1 0 0);
--color-tpop08-ink: #18281e;
--color-tpop08-ink: oklch(0.26 0.03 155);
--color-tpop08-mut: #506353;
--color-tpop08-mut: oklch(0.48 0.035 150);
--color-tpop08-rule: #dae0d7;
--color-tpop08-rule: oklch(0.9 0.015 135);
--color-tpop08-sage: #386649;
--color-tpop08-sage: oklch(0.47 0.07 155);
--font-tpop08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop08-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-08 {
width: 100%;
min-height: 100svh;
--color-tpop08-bg: color-mix(in oklch, oklch(0.17 0.012 150) calc(var(--tpop08-d) * 100%), oklch(0.95 0.012 130));
--color-tpop08-surface: color-mix(in oklch, oklch(0.215 0.014 150) calc(var(--tpop08-d) * 100%), oklch(1 0 0));
--color-tpop08-ink: color-mix(in oklch, oklch(0.94 0.012 140) calc(var(--tpop08-d) * 100%), oklch(0.26 0.03 155));
--color-tpop08-mut: color-mix(in oklch, oklch(0.73 0.03 145) calc(var(--tpop08-d) * 100%), oklch(0.48 0.035 150));
--color-tpop08-rule: color-mix(in oklch, oklch(0.31 0.016 150) calc(var(--tpop08-d) * 100%), oklch(0.9 0.015 135));
--color-tpop08-sage: color-mix(in oklch, oklch(0.76 0.09 155) calc(var(--tpop08-d) * 100%), oklch(0.47 0.07 155));
}
```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 Tooltip Arrow Pointing to Anchor
Source: https://codefronts.com/snippets/tailwind-css-popovers/tailwind-tooltip-arrow-pointing-to-anchor/
The bubble is easy; the tail is what breaks. Here the tail is the bubble's after: pseudo-element, itself anchor-positioned to the trigger with the same area and the same flip list, so it stays on the button's midline when the bubble is clamped or flipped. Every rule of it is an after: utility — gated so unanchored engines draw no tail rather than a wrong one.
## HTML
```html
<section class="tpop-08 transition-[--tpop08-d] duration-300 ease-in-out motion-reduce:transition-none dark:[--tpop08-d:1] [[data-theme=dark]_&]:[--tpop08-d:1] [[data-theme=light]_&]:[--tpop08-d:0] group/root w-full min-h-svh bg-tpop08-bg px-4.5 py-10 font-tpop08-sans text-tpop08-ink" aria-label="Tooltip arrow pointing to anchor demo">
<div class="grid min-h-[calc(100svh-80px)] max-w-full place-items-center">
<div class="w-full min-w-0 max-w-[27rem] rounded-2xl border border-tpop08-rule bg-tpop08-surface px-4.5 pt-5 pb-4.5 shadow-[0_14px_30px_-22px_oklch(0.3_0.03_150/0.45)]">
<p class="mb-3.5 text-[10.5px] font-bold uppercase tracking-[0.2em] text-tpop08-sage">Team roster</p>
<ul class="grid">
<li class="flex min-w-0 items-center gap-3 border-b border-tpop08-rule py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-sage/14 text-[11px] font-bold text-tpop08-sage">PS</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Priya Sharma</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Reviewer</span>
<button id="tpop-08-trigger" type="button" popovertarget="tpop-08-tip" aria-label="What reviewer permissions mean" aria-describedby="tpop-08-tip" class="inline-flex size-11 flex-none cursor-pointer items-center justify-center rounded-full border border-tpop08-rule text-tpop08-sage transition-colors [anchor-name:--tpop08-a] hover:bg-tpop08-bg focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:border-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:bg-tpop08-sage group-has-[#tpop-08-tip:popover-open]/root:text-tpop08-surface">
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="10" cy="10" r="7.2"/><path d="M10 9v5M10 6.3v.8"/></svg>
</button>
</li>
<li class="flex min-w-0 items-center gap-3 py-1.5">
<span aria-hidden="true" class="grid size-8 flex-none place-items-center rounded-full bg-tpop08-ink/8 text-[11px] font-bold text-tpop08-ink">SR</span>
<span class="min-w-0 flex-auto text-[14.5px] font-semibold">Sam Rivera</span>
<span class="flex-none rounded-full bg-tpop08-bg px-2 py-1 text-xs text-tpop08-mut">Owner</span>
<span aria-hidden="true" class="w-11 flex-none"></span>
</li>
</ul>
<p class="mt-4 text-[12.5px] leading-relaxed text-tpop08-mut">Open the hint, then narrow the frame — the tail follows the button, not the bubble.</p>
</div>
</div>
<div popover="auto" id="tpop-08-tip" role="tooltip" class="fixed m-auto w-[min(15rem,calc(100vw-28px))] rounded-2xl bg-tpop08-ink px-3.5 py-2.5 text-[13px] leading-normal text-tpop08-surface opacity-0 shadow-[0_12px_24px_-10px_oklch(0.3_0.03_150/0.45)] text-pretty transition-[opacity,display,overlay] duration-150 transition-discrete [position-anchor:--tpop08-a] [position-area:block-end_center] [justify-self:anchor-center] [position-try-fallbacks:flip-block] open:opacity-100 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-3 after:size-3 after:rotate-45 after:rounded-[3px] after:bg-tpop08-ink after:[position-anchor:--tpop08-a] after:[position-area:block-end_center] after:[justify-self:anchor-center] after:[position-try-fallbacks:flip-block] supports-[anchor-name:--a]:after:fixed supports-[anchor-name:--a]:after:inset-auto supports-[anchor-name:--a]:after:m-0 supports-[anchor-name:--a]:after:mt-1.5 supports-[anchor-name:--a]:after:content-['']">
Reviewers can comment and approve, but cannot merge or change settings.
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tpop08-bg: #ecf0e8;
--color-tpop08-bg: oklch(0.95 0.012 130);
--color-tpop08-surface: #ffffff;
--color-tpop08-surface: oklch(1 0 0);
--color-tpop08-ink: #18281e;
--color-tpop08-ink: oklch(0.26 0.03 155);
--color-tpop08-mut: #506353;
--color-tpop08-mut: oklch(0.48 0.035 150);
--color-tpop08-rule: #dae0d7;
--color-tpop08-rule: oklch(0.9 0.015 135);
--color-tpop08-sage: #386649;
--color-tpop08-sage: oklch(0.47 0.07 155);
--font-tpop08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Theme dial: 0 = light, 1 = dark. Registered so the root can transition it. */
@property --tpop08-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-08 {
width: 100%;
min-height: 100svh;
--color-tpop08-bg: color-mix(in oklch, oklch(0.17 0.012 150) calc(var(--tpop08-d) * 100%), oklch(0.95 0.012 130));
--color-tpop08-surface: color-mix(in oklch, oklch(0.215 0.014 150) calc(var(--tpop08-d) * 100%), oklch(1 0 0));
--color-tpop08-ink: color-mix(in oklch, oklch(0.94 0.012 140) calc(var(--tpop08-d) * 100%), oklch(0.26 0.03 155));
--color-tpop08-mut: color-mix(in oklch, oklch(0.73 0.03 145) calc(var(--tpop08-d) * 100%), oklch(0.48 0.035 150));
--color-tpop08-rule: color-mix(in oklch, oklch(0.31 0.016 150) calc(var(--tpop08-d) * 100%), oklch(0.9 0.015 135));
--color-tpop08-sage: color-mix(in oklch, oklch(0.76 0.09 155) calc(var(--tpop08-d) * 100%), oklch(0.47 0.07 155));
}
```How this works
Anchor the tail, not the bubble.
class="… [justify-self:anchor-center]
after:[position-anchor:--tpop08-a]
after:[position-area:block-end_center]
after:[justify-self:anchor-center]
after:[position-try-fallbacks:flip-block]
supports-[anchor-name:--a]:after:fixed
supports-[anchor-name:--a]:after:content-['']"A bubble wider than its trigger and clamped by the viewport is no longer centred over it, so a tail centred on the bubble points at nothing. A tail anchored to the trigger can't miss.
Why this bubble never animates transform. A transform, scale, translate or filter on the bubble would make it the containing block for its fixed ::after, and the tail would stop resolving against the viewport. The entry is opacity-only and the shadow is a box-shadow, not drop-shadow().
Keep the bubble plain. One line of hint, no interactive content — the mechanism is the tail staying glued through a placement change.
Make it yours
- Tail size: change
after:size-3, then keep the tail's margin at half its size so it overlaps the gap. - Above the trigger: flip both areas to
block-start_centerand usemb-3/after:mb-[6px]. - Sharper tip: swap the rotated square for
after:[clip-path:polygon(50%_0,100%_100%,0_100%)].
Gotchas — read before shipping
- A
scale/translateentry on the bubble breaks the tail — it becomes a containing block for fixed descendants. - Transparent-border triangles can't inherit a gradient or border; the rotated square can.
- The bubble and tail flip on their own overflow checks; keep the tail small so both thresholds coincide.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 125+ | 26+ | 131+ | 125+ |
Anchor-positioned pseudo-elements and anchor-center ship with anchor positioning (Chrome 125 / Safari 26 / Firefox 131). Without them the bubble opens centred and no tail is drawn — deliberately.