CSS Anchor Positioning
Tether tooltips and popovers to their trigger with pure CSS anchor-name and position-anchor, place them around the anchor via position-area, and let position-try-fallbacks flip them into view near a viewport edge. A live placement grid, an interactive playground, and a JS-driven fallback keep it working even in browsers without native support.
MCP
Code
:root {
--bg: #0c0d10;
--surface: #15161b;
--surface-2: #1d1f27;
--ink: #e9eaf0;
--muted: #9a9cab;
--accent: #8b5cf6;
--accent-2: #22d3ee;
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1200px 600px at 15% -10%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(900px 500px at 100% 0%, rgba(34, 211, 238, 0.1), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
}
code,
.code {
font-family: "JetBrains Mono", ui-monospace, monospace;
}
.wrap {
max-width: 860px;
margin: 0 auto;
padding: 48px 24px 72px;
}
/* ---------- Header ---------- */
.head {
margin-bottom: 36px;
}
.eyebrow {
margin: 0 0 10px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent-2);
}
h1 {
margin: 0 0 12px;
font-size: clamp(30px, 6vw, 44px);
font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(120deg, var(--ink), #b9a8ff 70%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lede {
margin: 0;
max-width: 60ch;
color: var(--muted);
font-size: 15.5px;
}
.lede code,
.panel-sub code {
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 6px;
padding: 1px 6px;
font-size: 0.86em;
}
.support {
margin: 18px 0 0;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
padding: 7px 12px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
}
.support::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.support.is-native {
color: var(--ok);
border-color: rgba(52, 211, 153, 0.35);
}
.support.is-native::before {
background: var(--ok);
box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.16);
}
.support.is-poly {
color: var(--warn);
border-color: rgba(251, 191, 36, 0.35);
}
.support.is-poly::before {
background: var(--warn);
box-shadow: 0 0 0 4px rgba(251, 191, 36, 0.16);
}
/* ---------- Panels ---------- */
.panel {
background: linear-gradient(180deg, var(--surface), rgba(21, 22, 27, 0.6));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 24px;
margin-top: 24px;
}
.panel-head {
margin-bottom: 22px;
}
.panel h2 {
margin: 0 0 6px;
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
}
.panel-sub {
margin: 0;
color: var(--muted);
font-size: 13.5px;
max-width: 56ch;
}
/* ---------- Placement grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
max-width: 420px;
margin: 40px auto;
}
.dot {
position: relative;
aspect-ratio: 1;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--muted);
font-family: inherit;
font-weight: 600;
font-size: 13px;
cursor: pointer;
display: grid;
place-items: center;
transition: border-color 0.15s, color 0.15s, transform 0.15s, background 0.15s;
anchor-name: none;
}
.dot:hover,
.dot:focus-visible {
color: var(--ink);
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
transform: translateY(-1px);
}
.dot:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.dot--center {
border-style: dashed;
}
.dot-label {
pointer-events: none;
}
/* The tooltip attached to each dot */
.tip {
position: absolute;
visibility: hidden;
opacity: 0;
transform: scale(0.94);
transform-origin: center;
white-space: nowrap;
background: var(--ink);
color: #0b0c0f;
font-size: 12px;
font-weight: 600;
padding: 6px 10px;
border-radius: var(--r-sm);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.45);
transition: opacity 0.14s ease, transform 0.14s ease;
z-index: 5;
margin: 8px; /* position-area gap */
}
.tip::after {
content: "";
position: absolute;
width: 8px;
height: 8px;
background: var(--ink);
rotate: 45deg;
left: 50%;
top: 100%;
translate: -50% -60%;
}
.dot:hover .tip,
.dot:focus-visible .tip {
visibility: visible;
opacity: 1;
transform: scale(1);
}
/* Native anchor positioning path (progressive enhancement) */
@supports (anchor-name: --a) {
.dot {
anchor-name: --grid-dot;
}
.tip {
position-anchor: --grid-dot;
position: absolute;
inset: auto;
}
/* placement per data-area */
.dot[data-area="top left"] .tip { position-area: top left; }
.dot[data-area="top"] .tip { position-area: top; }
.dot[data-area="top right"] .tip { position-area: top right; }
.dot[data-area="left"] .tip { position-area: left; }
.dot[data-area="center"] .tip { position-area: center; }
.dot[data-area="right"] .tip { position-area: right; }
.dot[data-area="bottom left"] .tip { position-area: bottom left; }
.dot[data-area="bottom"] .tip { position-area: bottom; }
.dot[data-area="bottom right"] .tip { position-area: bottom right; }
}
/* JS-polyfill path uses .tip--js absolute coords set inline */
.tip.tip--js::after {
display: none;
}
/* ---------- Playground ---------- */
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
margin-bottom: 22px;
}
.ctl {
display: flex;
flex-direction: column;
gap: 6px;
}
.ctl label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
select {
appearance: none;
background: var(--surface-2)
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%239a9cab' stroke-width='2'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E")
no-repeat right 12px center;
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 34px 9px 12px;
font-family: inherit;
font-size: 13.5px;
cursor: pointer;
}
select:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.switch {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
align-self: flex-end;
}
.switch input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.track {
width: 40px;
height: 22px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line-2);
position: relative;
transition: background 0.16s, border-color 0.16s;
}
.thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.16s, background 0.16s;
}
.switch input:checked + .track {
background: color-mix(in srgb, var(--accent) 45%, transparent);
border-color: var(--accent);
}
.switch input:checked + .track .thumb {
transform: translateX(18px);
background: var(--accent);
}
.switch input:focus-visible + .track {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.switch-text {
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
color: var(--ink);
}
.btn {
align-self: flex-end;
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 14px;
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.btn:hover {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, var(--surface-2));
}
.btn:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.stage {
position: relative;
height: 320px;
border-radius: var(--r-md);
border: 1px dashed var(--line-2);
background:
linear-gradient(var(--line) 1px, transparent 1px) 0 0 / 100% 32px,
linear-gradient(90deg, var(--line) 1px, transparent 1px) 0 0 / 32px 100%,
var(--bg);
overflow: hidden;
touch-action: none;
}
.anchor {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: linear-gradient(180deg, var(--accent), #6d3ff0);
color: #fff;
border: none;
border-radius: var(--r-md);
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: grab;
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.4);
anchor-name: --play-anchor;
z-index: 2;
white-space: nowrap;
}
.anchor:active {
cursor: grabbing;
}
.anchor.is-drag {
transition: none;
}
.anchor:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 3px;
}
.anchor-grip {
font-size: 16px;
opacity: 0.85;
line-height: 1;
}
.popover {
position: absolute;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
min-width: 190px;
background: var(--surface);
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
font-size: 13px;
z-index: 3;
pointer-events: none;
/* fallback offsets replaced when native supported */
left: 0;
top: 0;
margin: 10px;
}
.popover strong {
display: block;
font-size: 13px;
font-weight: 600;
}
.popover span {
color: var(--muted);
font-size: 12px;
}
.pop-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.18);
flex: none;
}
/* Native path: bind popover to the anchor + fallbacks */
@supports (anchor-name: --a) {
.popover {
position-anchor: --play-anchor;
position-area: var(--pa, bottom);
left: auto;
top: auto;
margin: 10px;
}
.stage[data-fallbacks="on"] .popover {
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
}
/* ---------- Code readout ---------- */
.code {
margin: 22px 0 0;
background: #0a0b0e;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px;
font-size: 12.5px;
line-height: 1.7;
color: #cdd0e0;
overflow-x: auto;
white-space: pre;
}
.code .tok-prop { color: var(--accent-2); }
.code .tok-val { color: #c4b5fd; }
.code .tok-sel { color: var(--ok); }
.code .tok-com { color: var(--muted); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.wrap {
padding: 32px 16px 56px;
}
.panel {
padding: 18px;
}
.grid {
gap: 10px;
}
.controls {
gap: 12px;
}
.ctl,
.switch,
.btn {
align-self: stretch;
}
.btn {
text-align: center;
}
.stage {
height: 280px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.001ms !important;
}
}(() => {
"use strict";
const NATIVE = CSS.supports("anchor-name: --x") &&
CSS.supports("position-area: top");
/* ---------------- Support badge ---------------- */
const badge = document.getElementById("support");
if (NATIVE) {
badge.textContent = "Native CSS anchor positioning — running the real feature";
badge.classList.add("is-native");
} else {
badge.textContent = "No native support — JS fallback polyfill active";
badge.classList.add("is-poly");
}
/* ---------------- Playground refs ---------------- */
const stage = document.getElementById("stage");
const anchor = document.getElementById("anchor");
const pop = document.getElementById("pop");
const placeSel = document.getElementById("place");
const fbToggle = document.getElementById("fallbacks");
const resetBtn = document.getElementById("reset");
const codeEl = document.getElementById("code");
// Anchor position as a fraction of the stage (kept in sync for both paths).
let ax = 0.5;
let ay = 0.5;
/* ---------------- Shared placement math ---------------- */
// Returns [dx, dy] unit-ish direction the popover sits relative to anchor.
function areaVector(area) {
const v = { top: [0, -1], bottom: [0, 1], left: [-1, 0], right: [1, 0] };
const parts = area.split(" ");
let dx = 0;
let dy = 0;
for (const p of parts) {
if (v[p]) {
dx += v[p][0];
dy += v[p][1];
}
}
return [dx, dy];
}
// Flip a placement across whichever axis is clipping.
function flipArea(area, flipX, flipY) {
const swap = { top: "bottom", bottom: "top", left: "right", right: "left" };
return area
.split(" ")
.map((p) => {
if ((flipY && (p === "top" || p === "bottom")) ||
(flipX && (p === "left" || p === "right"))) {
return swap[p];
}
return p;
})
.join(" ");
}
/* ---------------- Native path ---------------- */
function applyNative() {
const area = placeSel.value;
pop.style.setProperty("--pa", area);
stage.dataset.fallbacks = fbToggle.checked ? "on" : "off";
}
/* ---------------- Polyfill path ---------------- */
function positionManually() {
const sr = stage.getBoundingClientRect();
const ar = anchor.getBoundingClientRect();
const pr = pop.getBoundingClientRect();
const gap = 10;
let area = placeSel.value;
// Anchor box relative to stage.
const aLeft = ar.left - sr.left;
const aTop = ar.top - sr.top;
const aCx = aLeft + ar.width / 2;
const aCy = aTop + ar.height / 2;
function coords(a) {
const [dx, dy] = areaVector(a);
let x = aCx - pr.width / 2 + dx * (ar.width / 2 + pr.width / 2 + gap);
let y = aCy - pr.height / 2 + dy * (ar.height / 2 + pr.height / 2 + gap);
return [x, y];
}
let [x, y] = coords(area);
// Edge-flip when fallbacks are on.
if (fbToggle.checked) {
const overRight = x + pr.width > sr.width;
const overLeft = x < 0;
const overBottom = y + pr.height > sr.height;
const overTop = y < 0;
const flipX = overRight || overLeft;
const flipY = overBottom || overTop;
if (flipX || flipY) {
const flipped = flipArea(area, flipX, flipY);
const [fx, fy] = coords(flipped);
// Only adopt the flip if it actually improves fit on that axis.
if (fx >= 0 && fx + pr.width <= sr.width) x = fx;
if (fy >= 0 && fy + pr.height <= sr.height) y = fy;
area = flipped;
}
}
// Clamp so it never leaves the stage.
x = Math.max(4, Math.min(x, sr.width - pr.width - 4));
y = Math.max(4, Math.min(y, sr.height - pr.height - 4));
pop.style.margin = "0";
pop.style.left = `${x}px`;
pop.style.top = `${y}px`;
}
/* ---------------- Code readout ---------------- */
function renderCode() {
const area = placeSel.value;
const fb = fbToggle.checked;
const lines = [
['.anchor', "sel"],
[" anchor-name", "--play-anchor", "prop"],
["", "", "gap"],
['.popover', "sel"],
[" position-anchor", "--play-anchor", "prop"],
[" position-area", area, "prop"],
];
if (fb) {
lines.push([" position-try-fallbacks", "flip-block, flip-inline", "prop"]);
}
if (!NATIVE) {
lines.push(["", "", "gap"]);
lines.push(["/* no native support -> JS positioning */", "", "com"]);
}
let html = "";
for (const ln of lines) {
if (ln[1] === "sel") {
html += `<span class="tok-sel">${ln[0]}</span> {\n`;
} else if (ln[1] === "gap") {
html += "}\n\n";
} else if (ln[2] === "com") {
html += `<span class="tok-com">${escapeHtml(ln[0])}</span>\n`;
} else {
html += ` <span class="tok-prop">${ln[0].trim()}</span>: <span class="tok-val">${escapeHtml(ln[1])}</span>;\n`;
}
}
html += "}";
codeEl.innerHTML = html;
}
function escapeHtml(s) {
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]));
}
/* ---------------- Update loop ---------------- */
function place() {
// Position the anchor from ax/ay fractions.
const sr = stage.getBoundingClientRect();
const px = ax * sr.width;
const py = ay * sr.height;
anchor.style.left = `${px}px`;
anchor.style.top = `${py}px`;
if (NATIVE) {
applyNative();
pop.classList.remove("tip--js");
} else {
pop.classList.add("tip--js");
positionManually();
}
renderCode();
}
/* ---------------- Drag (pointer) ---------------- */
let dragging = false;
function startDrag(e) {
dragging = true;
anchor.classList.add("is-drag");
anchor.setPointerCapture?.(e.pointerId);
}
function moveDrag(e) {
if (!dragging) return;
const sr = stage.getBoundingClientRect();
ax = clamp((e.clientX - sr.left) / sr.width, 0.08, 0.92);
ay = clamp((e.clientY - sr.top) / sr.height, 0.1, 0.9);
place();
}
function endDrag() {
dragging = false;
anchor.classList.remove("is-drag");
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
anchor.addEventListener("pointerdown", startDrag);
window.addEventListener("pointermove", moveDrag);
window.addEventListener("pointerup", endDrag);
// Keyboard nudging for accessibility.
anchor.addEventListener("keydown", (e) => {
const step = e.shiftKey ? 0.1 : 0.03;
let handled = true;
if (e.key === "ArrowLeft") ax = clamp(ax - step, 0.08, 0.92);
else if (e.key === "ArrowRight") ax = clamp(ax + step, 0.08, 0.92);
else if (e.key === "ArrowUp") ay = clamp(ay - step, 0.1, 0.9);
else if (e.key === "ArrowDown") ay = clamp(ay + step, 0.1, 0.9);
else handled = false;
if (handled) {
e.preventDefault();
place();
}
});
/* ---------------- Controls ---------------- */
placeSel.addEventListener("change", place);
fbToggle.addEventListener("change", place);
resetBtn.addEventListener("click", () => {
ax = 0.5;
ay = 0.5;
place();
});
window.addEventListener("resize", place);
/* ---------------- Grid polyfill ---------------- */
// In non-supporting browsers, position the .tip elements manually on hover/focus.
if (!NATIVE) {
document.querySelectorAll(".dot").forEach((dot) => {
const tip = dot.querySelector(".tip");
tip.classList.add("tip--js");
const area = dot.dataset.area;
function pos() {
// Position relative to the dot (tip is absolutely placed inside it).
const [dx, dy] = areaVector(area);
const dw = dot.offsetWidth;
const dh = dot.offsetHeight;
const tw = tip.offsetWidth;
const th = tip.offsetHeight;
const gap = 8;
let x = dw / 2 - tw / 2 + dx * (dw / 2 + tw / 2 + gap);
let y = dh / 2 - th / 2 + dy * (dh / 2 + th / 2 + gap);
tip.style.left = `${x}px`;
tip.style.top = `${y}px`;
tip.style.margin = "0";
}
dot.addEventListener("pointerenter", pos);
dot.addEventListener("focus", pos);
});
}
/* ---------------- Init ---------------- */
place();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSS Anchor Positioning</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<header class="head">
<p class="eyebrow">CSS · Web Platform</p>
<h1>Anchor Positioning</h1>
<p class="lede">
Tooltips and popovers tethered to a trigger with pure CSS —
<code>anchor-name</code>, <code>position-anchor</code>,
<code>position-area</code> and
<code>position-try-fallbacks</code>. No coordinate math.
</p>
<p class="support" id="support" role="status" aria-live="polite">
Checking native support…
</p>
</header>
<!-- ============ PLACEMENT GRID ============ -->
<section class="panel" aria-labelledby="grid-title">
<div class="panel-head">
<h2 id="grid-title">Placement grid</h2>
<p class="panel-sub">
Hover or focus a dot — the tooltip attaches to it via
<code>position-area</code>. All nine placements shown.
</p>
</div>
<div class="grid" role="group" aria-label="Placement demo grid">
<!-- Each button is an anchor; its tooltip references it. -->
<button class="dot" data-area="top left" aria-describedby="tt-1">
<span class="dot-label">TL</span>
<span class="tip" id="tt-1" role="tooltip">top left</span>
</button>
<button class="dot" data-area="top" aria-describedby="tt-2">
<span class="dot-label">T</span>
<span class="tip" id="tt-2" role="tooltip">top</span>
</button>
<button class="dot" data-area="top right" aria-describedby="tt-3">
<span class="dot-label">TR</span>
<span class="tip" id="tt-3" role="tooltip">top right</span>
</button>
<button class="dot" data-area="left" aria-describedby="tt-4">
<span class="dot-label">L</span>
<span class="tip" id="tt-4" role="tooltip">left</span>
</button>
<button class="dot dot--center" data-area="center" aria-describedby="tt-5">
<span class="dot-label">C</span>
<span class="tip" id="tt-5" role="tooltip">center</span>
</button>
<button class="dot" data-area="right" aria-describedby="tt-6">
<span class="dot-label">R</span>
<span class="tip" id="tt-6" role="tooltip">right</span>
</button>
<button class="dot" data-area="bottom left" aria-describedby="tt-7">
<span class="dot-label">BL</span>
<span class="tip" id="tt-7" role="tooltip">bottom left</span>
</button>
<button class="dot" data-area="bottom" aria-describedby="tt-8">
<span class="dot-label">B</span>
<span class="tip" id="tt-8" role="tooltip">bottom</span>
</button>
<button class="dot" data-area="bottom right" aria-describedby="tt-9">
<span class="dot-label">BR</span>
<span class="tip" id="tt-9" role="tooltip">bottom right</span>
</button>
</div>
</section>
<!-- ============ PLAYGROUND ============ -->
<section class="panel" aria-labelledby="play-title">
<div class="panel-head">
<h2 id="play-title">Flip playground</h2>
<p class="panel-sub">
Drag the anchor toward an edge. With fallbacks on, the tooltip
flips to stay in view.
</p>
</div>
<div class="controls">
<div class="ctl">
<label for="place">Placement</label>
<select id="place">
<option value="top">top</option>
<option value="bottom" selected>bottom</option>
<option value="left">left</option>
<option value="right">right</option>
<option value="top left">top left</option>
<option value="top right">top right</option>
<option value="bottom left">bottom left</option>
<option value="bottom right">bottom right</option>
</select>
</div>
<label class="switch">
<input type="checkbox" id="fallbacks" checked />
<span class="track"><span class="thumb"></span></span>
<span class="switch-text">position-try-fallbacks</span>
</label>
<button class="btn" id="reset" type="button">Reset position</button>
</div>
<div class="stage" id="stage">
<button
class="anchor"
id="anchor"
type="button"
aria-describedby="pop"
aria-label="Draggable anchor. Use arrow keys to move, drag with pointer."
>
<span class="anchor-grip" aria-hidden="true">⠿</span>
Drag me
</button>
<div class="popover" id="pop" role="tooltip">
<span class="pop-dot" aria-hidden="true"></span>
<div>
<strong>Attached popover</strong>
<span>Follows the anchor · flips near edges</span>
</div>
</div>
</div>
<pre class="code" aria-label="Applied CSS"><code id="code"></code></pre>
</section>
</main>
<script src="script.js"></script>
</body>
</html>CSS Anchor Positioning
A demonstration of the CSS Anchor Positioning API — the browser feature that lets any absolutely-positioned element attach itself to an “anchor” element without JavaScript coordinate math. Each trigger declares an anchor-name, and its tooltip references it through position-anchor plus position-area, so the popover is laid out relative to the anchor’s box wherever it lands on the page.
The placement grid shows all nine position-area values (top/bottom/left/right and the corners, plus center) firing on hover and focus. The playground lets you pick a placement, toggle position-try-fallbacks, and drag the anchor toward a viewport edge to watch the tooltip automatically flip to the opposite side so it never clips. A live readout prints the exact CSS being applied.
When the browser lacks native support (detected with CSS.supports("anchor-name: --x")), a small vanilla-JS polyfill takes over: it measures the anchor with getBoundingClientRect(), positions the tooltip manually, and runs the same edge-flip logic — so the component degrades gracefully while the CSS path stays the primary implementation.