DEV Community

Cover image for CSS Anchor Positioning: Migrating shadcn/ui Off Radix
Dev Encyclopedia
Dev Encyclopedia

Posted on • Originally published at devencyclopedia.com

CSS Anchor Positioning: Migrating shadcn/ui Off Radix

CSS Anchor Positioning and the native Popover API reached full Baseline support across major browsers in 2026. That means shadcn/ui's Popover and Tooltip, both built on Radix UI primitives, can now be rebuilt with zero JavaScript for positioning.

Radix (via Floating UI) has always handled the hard part: measuring the trigger, calculating placement, listening for scroll/resize, flipping on overflow. That required JS because the browser couldn't do it natively. Now it can.

What you gain and lose

Dimension Radix + Floating UI Native CSS Anchor Positioning
Bundle size Adds a dependency per component Zero JS for positioning
Runtime cost Recalculates on scroll/resize Browser handles it natively
Viewport flip Floating UI's flip() middleware position-try-fallbacks
Focus trapping Built in You implement it, or keep Radix
Keyboard nav Built into Radix Manual for complex cases
Browser support Works everywhere JS runs Needs Baseline 2026 + Safari fallback

For a simple Tooltip or a Popover holding text and a button, native CSS wins outright. For DropdownMenu or Select, keep Radix, the focus trapping and keyboard navigation aren't worth rebuilding by hand yet.

The shape of the migration

The native Popover component uses anchor-name to mark the trigger, anchor() to position the content relative to it, and the browser's built-in popover attribute for top-layer rendering, light-dismiss, and Escape-to-close:

const PopoverContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, style, ...props }, ref) => {
    const ctx = React.useContext(PopoverContext)
    return (
      <div
        ref={ref}
        id={ctx.contentId}
        popover="auto"
        style={{
          positionAnchor: `--${ctx.triggerId}`,
          top: `anchor(--${ctx.triggerId} bottom)`,
          positionTryFallbacks: "flip-block, flip-inline",
          ...style,
        }}
        {...props}
      />
    )
  }
)
Enter fullscreen mode Exit fullscreen mode

No Portal, no scroll/resize listeners, no Floating UI middleware to configure. popover="auto" gives light-dismiss and Escape-to-close for free. Tooltip follows the same pattern with popover="hint" instead.

One catch: Safari 18.2-18.3 support anchor() but not position-try-fallbacks. Write your base position as a plain value outside the fallback block and it degrades gracefully, no @supports check needed.

Adoption order

  1. Tooltip first - lowest risk, no focus management to preserve
  2. Simple Popovers next - text, single button, one-field forms
  3. Leave DropdownMenu and Select on Radix - the keyboard nav and focus trapping rebuild isn't worth it yet
  4. Measure bundle size before and after each migration to justify the effort

The full post has the complete before/after code for both components, the Safari fallback pattern in detail, and what you still need Radix for:

👉 CSS Anchor Positioning: Migrating shadcn/ui Off Radix

Top comments (0)