I Replaced Three JS Libraries With Just HTML Invoker Commands and CSS Anchor Positioning
Last week I was building a dropdown menu. A simple one — click a button, show a menu, click outside to close it. I've done this maybe fifty times in my career. First with jQuery, then with React state and refs, then with headless UI libraries. Every single time, the same cycle: install a package, fight with the API, realize it doesn't handle edge cases, write more code to fix it.
This time, I didn't install anything. I wrote zero JavaScript. And it worked.
The combination that made this possible is the Invoker Commands API plus the Popover API plus CSS Anchor Positioning. All three are baseline in every major browser as of early 2026. If you haven't played with them yet, you're writing more code than you need to.
What Invoker Commands Actually Do
Invoker Commands are a set of command and commandfor attributes on buttons. They let you control dialogs and popovers declaratively: no onclick, no addEventListener, no JavaScript at all.
Here's the dropdown I built:
<button commandfor="user-menu" command="toggle-popover">
Settings
</button>
<div id="user-menu" popover>
<nav>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<a href="/logout">Logout</a>
</nav>
</div>
That's it. The commandfor attribute points to the popover's ID. The command attribute says what to do — in this case, toggle-popover. Clicking the button toggles the popover. Clicking outside closes it. The browser handles focus management, top-layer rendering, and dismissal. I didn't write a single line of JS.
There are other commands too: show-modal and close for dialogs, show-popover and hide-popover for popovers. The request-close command is interesting — it fires a cancel event, so you can intercept the close and do something before it actually closes. Handy for "are you sure?" dialogs.
The Positioning Problem
So the dropdown works, but it's positioned at the top of the viewport. That's the default for popovers — they render in the top layer, which means they appear above everything, but they also appear at the top of the screen by default. Not super useful for a dropdown menu that should appear below the button.
Enter CSS Anchor Positioning:
#user-menu {
position-anchor: --menu-btn;
top: anchor(bottom);
left: anchor(left);
margin-top: 4px;
}
<button commandfor="user-menu" command="toggle-popover"
style="anchor-name: --menu-btn">
Settings
</button>
This positions the popover relative to the button. anchor(bottom) means "align my top edge with the button's bottom edge." anchor(left) means "align my left edge with the button's left edge." The result is a dropdown that looks and behaves like every dropdown you've ever used — except it's all HTML and CSS.
I'll be honest: the anchor positioning syntax took me a minute to get used to. anchor(bottom) returns the bottom edge of the anchor element, so when you set top: anchor(bottom), you're saying "my top goes where the anchor's bottom is." It's backwards from how I usually think about positioning. But once you write it a few times, it clicks.
Tooltips Without JavaScript
I replaced a tooltip library too. The pattern is similar but with popover="manual" and hover triggers:
<button popover="manual" id="tip-1"
style="anchor-name: --tip-anchor"
onmouseenter="document.getElementById('tip-1').showPopover()"
onmouseleave="document.getElementById('tip-1').hidePopover()">
Hover me
</button>
<div id="tip-1" role="tooltip" popover="manual"
style="position-anchor: --tip-anchor;
bottom: anchor(top);
left: anchor(center);
translate: -50% 0;
margin-bottom: 6px;">
This is a tooltip
</div>
Wait — that has inline event handlers. That's JS. Look, I'm not a purist. The onmouseenter and onmouseleave attributes are technically JavaScript, but they're declarative: no event listeners, no query selectors, no useEffect. If you want zero JS at all, you could use CSS :hover with the :popover-open pseudo-class, but that has accessibility issues. A tiny bit of inline JS for hover behavior is a reasonable trade.
I save patterns like this in Snippet Ark so I don't have to remember the exact syntax next time. It's one of those things you'll use once a month and forget the details — having it saved as a snippet saves me the Google search.
The Gotchas
It's not all sunshine. Here are the things that bit me:
Popover stacking. If you open one popover and then another, the second one closes the first. That's by design — the top layer only shows one popover at a time by default. Use popover="manual" if you need nested menus, but then you lose the light-dismiss behavior (clicking outside to close). You'll need to handle that yourself.
Anchor positioning and overflow. If the button is near the bottom of the viewport, the dropdown will still render relative to the anchor — it won't automatically flip upward. CSS Anchor Positioning has position-try-fallbacks for this, but the syntax is verbose and browser support is still inconsistent. I ended up adding a simple check in CSS:
@position-try --flip-up {
top: anchor(top);
bottom: auto;
margin-top: 0;
margin-bottom: 4px;
}
Invoker Commands only work on buttons. You can't put commandfor on a div or a span. This makes sense from a semantic standpoint — only interactive elements should trigger behavior — but it means you can't use this pattern for custom trigger elements without wrapping them in a button.
What I Actually Removed
I had three packages in my project that I could delete:
- A dropdown/select component library (~8KB gzipped)
- A tooltip library (~4KB gzipped)
- A modal library that I was already partially replacing with native dialog (~12KB gzipped)
Total: about 24KB of JavaScript, gone. Not a life-changing amount, but every KB matters when you're building for mobile. More importantly, I removed the maintenance burden: no more version bumps, no more breaking API changes, no more "why did this work last week" moments.
I did keep a few things. Complex menus with keyboard navigation still need JS. Accessible autocomplete comboboxes are still a nightmare of ARIA attributes and focus management that native HTML doesn't fully cover. But for the common cases — dropdowns, tooltips, popovers, simple dialogs — the platform is good enough now.
Should You Do This?
If you're starting a new project today, absolutely. There's no reason to install a library for a simple dropdown or tooltip when the browser handles it natively. The code is smaller, faster, and more accessible out of the box.
If you're maintaining an existing project, it depends. Replacing a well-tested third-party library with native APIs is a trade-off. You gain performance and reduce dependencies, but you lose browser fallback (older Safari versions don't support Invoker Commands) and maybe some edge case handling that your library already solved. I'd only do it if the library is causing pain — bundle size issues, upgrade headaches, or accessibility problems.
Me? I'm going through my projects one by one and ripping out anything that the browser can now do natively. It's surprisingly satisfying. Kind of like spring cleaning, but for my package.json.
If you want to keep these patterns handy for your next project, Snippet Ark is where I store mine. Having them a quick search away means I actually remember to use them instead of defaulting to the old npm install reflex.