20 CSS Popovers03 / 20

Pure CSSMIT licensed

Basic Anchored Popover

The two features compose but ship separately: popover handles the top layer and dismissal, anchor positioning handles where the panel lands. Name the trigger with anchor-name, point the panel at it with position-anchor, pick a side with position-area, and the entire measure-and-reposition dance that positioning libraries exist for disappears into four declarations.

Published

Live Demo
Try it

The code

<div class="pop-03">
  <div class="pop-03__stage">
    <div class="pop-03__card">
      <p class="pop-03__kicker">Checkout · Order 40118</p>
      <h2 class="pop-03__h">Two features, one panel</h2>
      <table class="pop-03__table">
        <tbody>
          <tr class="pop-03__row"><th class="pop-03__th" scope="row">Subtotal</th><td class="pop-03__td">$248.00</td></tr>
          <tr class="pop-03__row">
            <th class="pop-03__th" scope="row">
              <button class="pop-03__chip" type="button" popovertarget="pop-03-panel">
                Duties estimate
                <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 7.2v4M8 5.2v.6"/></svg>
              </button>
            </th>
            <td class="pop-03__td">$19.84</td>
          </tr>
          <tr class="pop-03__row"><th class="pop-03__th" scope="row">Shipping</th><td class="pop-03__td">$0.00</td></tr>
          <tr class="pop-03__row pop-03__row--total"><th class="pop-03__th" scope="row">Total</th><td class="pop-03__td">$267.84</td></tr>
        </tbody>
      </table>
      <p class="pop-03__foot">The chip is the anchor. The panel finds it by name.</p>
    </div>
  </div>

  <div class="pop-03__panel" popover="auto" id="pop-03-panel" role="group" aria-label="How duties are estimated">
    <p class="pop-03__ptitle">How this is estimated</p>
    <p class="pop-03__pbody">8% of the declared value for goods entering the EU under DDP terms. Final duties are set at the border and may differ by a few cents.</p>
    <dl class="pop-03__dl">
      <div class="pop-03__dlrow"><dt class="pop-03__dt">Basis</dt><dd class="pop-03__dd">$248.00</dd></div>
      <div class="pop-03__dlrow"><dt class="pop-03__dt">Rate</dt><dd class="pop-03__dd">8.0%</dd></div>
    </dl>
  </div>
</div>
.pop-03 {
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  --paper: #f2ebdd;
  --pop: #fffdf7;
  --ink: #241f16;
  --muted: #6f6350;
  --edge: #241f16;
  --accent: #b4451f;
  --font-ui: "Iowan Old Style", Georgia, "Times New Roman", serif;
  --font-alt: ui-sans-serif, system-ui, sans-serif;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-alt);
  padding: 40px 18px;
}

.pop-03__stage {
  display: grid;
  place-items: center;
  max-inline-size: 100%;
  min-height: calc(100vh - 80px);
}

.pop-03__card {
  inline-size: 100%;
  max-inline-size: 30rem;
  min-inline-size: 0;
  background: var(--pop);
  border: 2px solid var(--edge);
  padding: 22px 20px 20px;
  box-shadow: 8px 8px 0 var(--edge);
}

.pop-03__kicker {
  margin: 0 0 8px;
  font: 700 10.5px/1 var(--font-alt);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
}

.pop-03__h {
  margin: 0 0 20px;
  font: 400 clamp(22px, 5vw, 30px)/1.15 var(--font-ui);
  letter-spacing: -0.01em;
}

.pop-03__table {
  inline-size: 100%;
  border-collapse: collapse;
}

.pop-03__row {
  border-block-end: 1px solid rgba(36, 31, 22, 0.16);
}

.pop-03__th {
  text-align: start;
  padding: 11px 0;
  font: 400 14px/1.4 var(--font-alt);
  color: var(--muted);
  min-inline-size: 0;
}

.pop-03__td {
  text-align: end;
  padding: 11px 0;
  font: 500 14px/1.4 var(--font-alt);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pop-03__row--total {
  border-block-end: 0;
}

.pop-03__row--total .pop-03__th,
.pop-03__row--total .pop-03__td {
  font-weight: 700;
  color: var(--ink);
  font-size: 15px;
}

.pop-03__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 11px;
  font: 600 13px/1 var(--font-alt);
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--edge);
  cursor: pointer;
  anchor-name: --pop-03-anchor;
}

.pop-03__chip:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.pop-03__foot {
  margin: 16px 0 0;
  font-size: 12px;
  color: var(--muted);
}

.pop-03__panel {
  position: fixed;
  margin: 0;
  inline-size: min(20rem, calc(100vw - 32px));
  max-inline-size: calc(100vw - 32px);
  padding: 16px;
  color: var(--ink);
  background: var(--pop);
  border: 2px solid var(--edge);
  box-shadow: 6px 6px 0 var(--edge);
}

@supports (anchor-name: --a) {
  .pop-03__panel {
    position-anchor: --pop-03-anchor;
    position-area: block-end span-inline-end;
    inset: auto;
    margin-block-start: 10px;
    position-try-fallbacks: flip-block, flip-inline;
  }
}

.pop-03__ptitle {
  margin: 0 0 8px;
  font: 700 10.5px/1 var(--font-alt);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}

.pop-03__pbody {
  margin: 0 0 14px;
  font: 400 13.5px/1.6 var(--font-alt);
  color: var(--muted);
  text-wrap: pretty;
}

.pop-03__dl {
  margin: 0;
  display: grid;
  gap: 6px;
  border-block-start: 2px solid var(--edge);
  padding-block-start: 10px;
}

.pop-03__dlrow {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  min-inline-size: 0;
}

.pop-03__dt {
  margin: 0;
  font-size: 12.5px;
  color: var(--muted);
}

.pop-03__dd {
  margin: 0;
  font-size: 12.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

@media (prefers-color-scheme: dark) {
  .pop-03 {
    --paper: #191510;
    --pop: #221d16;
    --ink: #f4ece0;
    --muted: #a89880;
    --edge: #f4ece0;
    --accent: #e58a5c;
  }
}

[data-theme="dark"] .pop-03 {
  --paper: #191510;
  --pop: #221d16;
  --ink: #f4ece0;
  --muted: #a89880;
  --edge: #f4ece0;
  --accent: #e58a5c;
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Popover 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: Basic Anchored Popover
Source: https://codefronts.com/snippets/css-popovers/basic-anchored-popover/

The two features compose but ship separately: popover handles the top layer and dismissal, anchor positioning handles where the panel lands. Name the trigger with anchor-name, point the panel at it with position-anchor, pick a side with position-area, and the entire measure-and-reposition dance that positioning libraries exist for disappears into four declarations.
## HTML
```html
<div class="pop-03">
  <div class="pop-03__stage">
    <div class="pop-03__card">
      <p class="pop-03__kicker">Checkout · Order 40118</p>
      <h2 class="pop-03__h">Two features, one panel</h2>
      <table class="pop-03__table">
        <tbody>
          <tr class="pop-03__row"><th class="pop-03__th" scope="row">Subtotal</th><td class="pop-03__td">$248.00</td></tr>
          <tr class="pop-03__row">
            <th class="pop-03__th" scope="row">
              <button class="pop-03__chip" type="button" popovertarget="pop-03-panel">
                Duties estimate
                <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 7.2v4M8 5.2v.6"/></svg>
              </button>
            </th>
            <td class="pop-03__td">$19.84</td>
          </tr>
          <tr class="pop-03__row"><th class="pop-03__th" scope="row">Shipping</th><td class="pop-03__td">$0.00</td></tr>
          <tr class="pop-03__row pop-03__row--total"><th class="pop-03__th" scope="row">Total</th><td class="pop-03__td">$267.84</td></tr>
        </tbody>
      </table>
      <p class="pop-03__foot">The chip is the anchor. The panel finds it by name.</p>
    </div>
  </div>

  <div class="pop-03__panel" popover="auto" id="pop-03-panel" role="group" aria-label="How duties are estimated">
    <p class="pop-03__ptitle">How this is estimated</p>
    <p class="pop-03__pbody">8% of the declared value for goods entering the EU under DDP terms. Final duties are set at the border and may differ by a few cents.</p>
    <dl class="pop-03__dl">
      <div class="pop-03__dlrow"><dt class="pop-03__dt">Basis</dt><dd class="pop-03__dd">$248.00</dd></div>
      <div class="pop-03__dlrow"><dt class="pop-03__dt">Rate</dt><dd class="pop-03__dd">8.0%</dd></div>
    </dl>
  </div>
</div>
```
## CSS
```css
.pop-03 {
  width: 100%;
  min-height: 100vh;
  display: block;
  box-sizing: border-box;
  --paper: #f2ebdd;
  --pop: #fffdf7;
  --ink: #241f16;
  --muted: #6f6350;
  --edge: #241f16;
  --accent: #b4451f;
  --font-ui: "Iowan Old Style", Georgia, "Times New Roman", serif;
  --font-alt: ui-sans-serif, system-ui, sans-serif;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-alt);
  padding: 40px 18px;
}

.pop-03__stage {
  display: grid;
  place-items: center;
  max-inline-size: 100%;
  min-height: calc(100vh - 80px);
}

.pop-03__card {
  inline-size: 100%;
  max-inline-size: 30rem;
  min-inline-size: 0;
  background: var(--pop);
  border: 2px solid var(--edge);
  padding: 22px 20px 20px;
  box-shadow: 8px 8px 0 var(--edge);
}

.pop-03__kicker {
  margin: 0 0 8px;
  font: 700 10.5px/1 var(--font-alt);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
}

.pop-03__h {
  margin: 0 0 20px;
  font: 400 clamp(22px, 5vw, 30px)/1.15 var(--font-ui);
  letter-spacing: -0.01em;
}

.pop-03__table {
  inline-size: 100%;
  border-collapse: collapse;
}

.pop-03__row {
  border-block-end: 1px solid rgba(36, 31, 22, 0.16);
}

.pop-03__th {
  text-align: start;
  padding: 11px 0;
  font: 400 14px/1.4 var(--font-alt);
  color: var(--muted);
  min-inline-size: 0;
}

.pop-03__td {
  text-align: end;
  padding: 11px 0;
  font: 500 14px/1.4 var(--font-alt);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pop-03__row--total {
  border-block-end: 0;
}

.pop-03__row--total .pop-03__th,
.pop-03__row--total .pop-03__td {
  font-weight: 700;
  color: var(--ink);
  font-size: 15px;
}

.pop-03__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 11px;
  font: 600 13px/1 var(--font-alt);
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--edge);
  cursor: pointer;
  anchor-name: --pop-03-anchor;
}

.pop-03__chip:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.pop-03__foot {
  margin: 16px 0 0;
  font-size: 12px;
  color: var(--muted);
}

.pop-03__panel {
  position: fixed;
  margin: 0;
  inline-size: min(20rem, calc(100vw - 32px));
  max-inline-size: calc(100vw - 32px);
  padding: 16px;
  color: var(--ink);
  background: var(--pop);
  border: 2px solid var(--edge);
  box-shadow: 6px 6px 0 var(--edge);
}

@supports (anchor-name: --a) {
  .pop-03__panel {
    position-anchor: --pop-03-anchor;
    position-area: block-end span-inline-end;
    inset: auto;
    margin-block-start: 10px;
    position-try-fallbacks: flip-block, flip-inline;
  }
}

.pop-03__ptitle {
  margin: 0 0 8px;
  font: 700 10.5px/1 var(--font-alt);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}

.pop-03__pbody {
  margin: 0 0 14px;
  font: 400 13.5px/1.6 var(--font-alt);
  color: var(--muted);
  text-wrap: pretty;
}

.pop-03__dl {
  margin: 0;
  display: grid;
  gap: 6px;
  border-block-start: 2px solid var(--edge);
  padding-block-start: 10px;
}

.pop-03__dlrow {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  min-inline-size: 0;
}

.pop-03__dt {
  margin: 0;
  font-size: 12.5px;
  color: var(--muted);
}

.pop-03__dd {
  margin: 0;
  font-size: 12.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

@media (prefers-color-scheme: dark) {
  .pop-03 {
    --paper: #191510;
    --pop: #221d16;
    --ink: #f4ece0;
    --muted: #a89880;
    --edge: #f4ece0;
    --accent: #e58a5c;
  }
}

[data-theme="dark"] .pop-03 {
  --paper: #191510;
  --pop: #221d16;
  --ink: #f4ece0;
  --muted: #a89880;
  --edge: #f4ece0;
  --accent: #e58a5c;
}
```

How this works

Anchor positioning is a two-sided contract. The trigger declares anchor-name: --pop-03-anchor, a dashed-ident it invents; the panel declares position-anchor: --pop-03-anchor. Miss either side and nothing happens — the panel falls back to the UA's centred placement, which is the single most common reason a first attempt looks broken.

position-area does the work a library did. One value like block-end center means below, horizontally centred: the browser resolves it against the anchor's box every frame, through scrolling and resizing, with no measurement code. Reset the UA's inset: 0 to auto and set margin: 0 first, or the panel stretches across the whole region instead of hugging the anchor.

The two features are independent, and that matters for support. Popover works in every current engine; anchor positioning does not. A browser can have popover and no anchor positioning, so gate the tethering behind @supports (anchor-name: --a) and declare a sensible unanchored placement first. This demo centres the panel in that case — still usable, just not tethered.

Do not reach for absolute plus top and left. Hand-computed offsets are exactly what this API replaces, and they break the moment the anchor scrolls inside a container or the panel would leave the viewport. Add position-try-fallbacks instead and let the browser pick another side; demo 07 takes that apart.

Make it yours

  • Change position-area to block-start center to place the panel above its trigger.
  • Widen the anchor gap by raising the panel's margin-block-start.
  • Swap the offset shadow for none and keep the 2px border for a flatter brutalist read.
  • Add position-area: inline-end center for a side panel instead of a dropdown.
  • Change --pop and --edge on the root to move from cream to any paper tone.
  • Set min-inline-size: anchor-size(width) to make the panel at least as wide as its trigger — demo 09 goes further.

Gotchas — read before shipping

  • Declaring position-anchor without a matching anchor-name on a visible element silently reverts to the centred UA placement, with no console warning.
  • Leaving the UA's inset: 0 and margin: auto in place makes position-area look like it is being ignored, because the panel is being stretched and centred inside the region.
  • An anchor name is scoped to the tree it is declared in, so two demos on one page must use distinct names or the wrong anchor wins.
  • Popover support does not imply anchor support — testing only for popover and assuming the panel is tethered is the mismatch demo 16 is about.

Browser support

ChromeSafariFirefoxEdge
125+26+131+125+

Two separate gates apply. The popover attribute is Chrome 114, Safari 17, Firefox 125; anchor positioning is Chrome 125, Safari 26 and Firefox 131, so the version floor above is set by anchoring rather than by popover. Behind the @supports gate the panel still opens and dismisses correctly, it just appears centred instead of tethered.

Techniques used in this demo

Search CodeFronts

Loading…