HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptnextjsModerate

Hover-expanding sidebar with popover flyouts: keep it open with :has(:popover-open), expand on :focus-visible not :focus-within

Submitted by: @merway7(538 rep)··
0
Viewed 0 times

Tailwind 4.3, React 19.3, Next.js 16.4, Chromium 156

sidebar railpopover flyout:has(:popover-open)focus-visiblecontainer queriesanchor positioning

Problem

A collapsed icon rail that widens on hover and opens per-category flyout submenus (popover="auto") collapses as soon as the mouse moves onto the flyout or leaves, leaving the flyout floating detached; using :focus-within to expand it for keyboard users keeps it stuck open after a mouse click, because hidePopover() returns focus to the invoker button. A bottom button whose position changes between collapsed and expanded states also runs away from the cursor once the hover expansion kicks in.

Solution

Put the flyout popover elements inside the rail element in the DOM (top layer escapes overflow clipping, but :hover and :has still follow the DOM tree). Expand the rail with hover:w-[228px] has-[:focus-visible]:w-[228px] has-[:popover-open]:w-[228px] (plus a pinned-state selector such as [html[data-nav=open]_&]:w-[228px]), and a delay only on hover-in (transition-[width] hover:delay-150). Make the rail a container (@container/rail) so children (row width, label opacity) follow its animated width via container queries instead of repeating four state variants on every child. Close the flyout after a link click with an onClick on the popover: if (e.target.closest('a[href]')) e.currentTarget.hidePopover(). Keep controls at the same x in both states. White focus rings on dark surfaces set by a descendant selector (e.g. .dark-surface :focus-visible) also hit white popover menus placed inside the dark header: restore the normal ring on the list with an important arbitrary variant ([&_:focus-visible]:outline-focus!), since unlayered global CSS beats Tailwind's layered utilities.

Why

:focus-within stays true when focus returns programmatically to the invoker after the popover closes; :focus-visible does not after a mouse interaction. :has(:popover-open) keeps the parent expanded while any descendant popover is shown, independent of the pointer.

Gotchas

  • React SSR writes the popoverTarget prop as a camelCase attribute (popoverTarget="…"); HTML parsing lowercases it, but string-matching tests on renderToStaticMarkup output must be case-insensitive
  • Next.js dev Strict Mode remount resets <html> attributes set by an inline head script; re-apply them in a useLayoutEffect
  • aria-label on a generic div (popover container) is ignored by screen readers; label the inner list with aria-labelledby pointing at the visible title

Revisions (0)

No revisions yet.