Position Aware Tooltips

When building tooltips or popovers with CSS Anchor Positioning, the browser can automatically "flip" the element to a fallback position if it would otherwise overflow the viewport. When this happens, you may want to adjust the style of the positioned content, for instance to reposition an arrow that points from the positioned content to the anchor.

Anchored Container Queries solve this by allowing you to query the active positioning state of an element and apply styles accordingly. For general anchored dropdown and flyout positioning with viewport collision flipping (without directional arrow restyling), see Viewport-Aware Dropdowns.

The problem

Imagine a tooltip that appears above its anchor by default. It has a "down" arrow at the bottom. If the user scrolls and the tooltip flips to appear below the anchor, the arrow is now pointing the wrong way and is on the wrong side of the tooltip.

The solution: Anchored Container Queries

By setting container-type: anchored on your positioned element, you turn it into a query container that knows about its own anchor-positioned state. You can then use the @container anchored() query to update its descendants or pseudo-elements.

1. Create the tooltip and trigger

Use the Popover API to create a tooltip. Invoking the popover via popovertarget automatically establishes an implicit anchor connection with the trigger button, so no explicit anchor-name or position-anchor is required.

<button popovertarget="tooltip" id="anchor" aria-describedby="tooltip">anchor</button>
<div id="tooltip" popover role="tooltip"></div>

Reset the popover inset and margin styles for use with anchor positioning, but only if anchor positioning is supported.

@supports (anchor-name: --my-anchor) {
  [popover] {
    inset: auto;
    margin: unset;
  }
}

2. Set up the container

Apply container-type: anchored to the element being positioned. This element must also have position-try-fallbacks defined to enable the flipping behavior.

#tooltip {
  position: fixed;
  position-area: block-start;
  position-try-fallbacks: flip-block;

  /* Enable anchored container queries */
  container-type: anchored;
}

3. Style based on the fallback

Use @container anchored(fallback: <value>) to apply styles when a specific fallback is active.

Like all container queries, @container can only style descendants of the container. A common strategy to create the arrows is with the ::before and ::after pseudo-elements, which are treated as descendants and can be styled directly. However, to style the tooltip itself (as seen in step 4), we will add a child element to the tooltip, and create the arrow in its ::before pseudo-element.

<div id="tooltip" popover role="tooltip">
  <div class="tooltip-content">Tooltip</div>
</div>
.tooltip-content::before {
  /* Default "down" arrow for the 'top' position */
  content: "▼";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 1rem;
}

/* Update to an "up" arrow when the 'flip-block' fallback (bottom) is active */
@container anchored(fallback: flip-block) {
  .tooltip-content::before {
    content: "▲";
    inset-block-start: 0;
    inset-block-end: auto;
  }
}

4. Styling the container itself

If you need to change properties on the container itself (like margin or background-color) when it flips, you should use an inner wrapper element.

  1. Apply container-type: anchored to the outer positioned element.
  2. Target the inner element inside the @container block.
@container anchored(fallback: flip-block) {
  .tooltip-content {
    border-radius: 0 0 .5rem .5rem;
    margin-block-start: 0.25rem;
  }
}

Best practices

  • Prefer logical fallbacks: Use keywords like flip-block and flip-inline in position-try-fallbacks for simpler queries that handle RTL and different writing modes automatically.
  • Use pseudo-elements for arrows: Tooltip arrows are purely decorative and are perfect candidates for ::before or ::after, which can be styled via anchored container queries without extra DOM.

Fallback strategies

Positioning the arrow based on the applied fallback is a progressive enhancement, and there is not another way of reacting to the fallback position. To hide the arrow in browsers that don't support anchor position container queries, test for CSS support with @supports (container-type: anchored).

@supports (container-type: anchored) {
  .tooltip-content::before {
    content: "▼";
  }
}

Fallbacks & browser support for Popover

The Popover API is mostly progressive enhancement, but its defining behaviors — top-layer promotion, light-dismiss, and popovertarget invocation — have no CSS-only equivalent. Older browsers need a polyfill, or a manual fallback if you would rather not ship one.

Polyfill: To support the popover attribute in older browsers, conditionally load @oddbird/popover-polyfill. MANDATORY: Feature detect by checking for the popover property on HTMLElement.prototype, and load the polyfill only when native support is missing — do NOT load it unconditionally.

With a bundler or import map:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Without a bundler, import from a CDN inside a <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Styling caveat: The polyfill cannot define the real :popover-open pseudo-class, so it applies a .\:popover-open class instead. MANDATORY: Combine the two with :is() or :where(), otherwise browsers that lack :popover-open discard the entire rule:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

Alternatively, for a legacy fallback without a polyfill, use position: fixed and manually calculate coordinates via getBoundingClientRect() or rely on default positioning with inset: auto if that's acceptable for the use case.

Browsers without support for the Popover API also do not support anchor positioning, so the tooltip will appear in the center of the screen.