Skip to content

feat(studio): a popover can point at its trigger with an optional arrow - #4758

Merged
miguel-heygen merged 4 commits into
mainfrom
studio/popover-arrow
Sep 30, 2026
Merged

miguel-heygen merged 4 commits into
mainfrom
studio/popover-arrow

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

Popover takes an optional arrow prop. When set, the popup draws a small arrow pointing at its trigger, using Base UI's Popover.Arrow, filled with the popup's surface and outlined with its border color, rotated to whichever side the popup landed on after collision flips. The default gap from the trigger grows by the arrow's height so the tip keeps the usual 6 px. Off by default: every existing popover renders exactly as before.

Why

A popover opened from a small trigger in a dense surface (a chip on a timeline clip) reads as floating; the arrow ties it to the thing it edits.

What I measured

  • Menu.test.tsx, new case "points at its trigger only when asked": the popup opens without an arrow by default; with arrow, the arrow renders and carries the popup's data-side. With main's Popover.tsx put back, it fails (arrow not rendered).
  • Menu.test.tsx and styles/tokenGate.test.ts: 17 of 17 pass. Each new data-[side=*]: class was also compiled directly with Studio's Tailwind setup, since the token gate checks a class with its variant removed.
  • tsc --noEmit in packages/studio, oxlint and oxfmt --check on both files: clean.
  • A left or right arrow is 14 px along the popup's edge once rotated, so arrowPadding is 10 to keep it off the rounded corner when the popup is shifted against the viewport.

Before

A throwaway page rendering Popover open on each side, from main: no arrow.

Before: popovers on four sides, no arrow

After

The same page with arrow: each popup points at its trigger, and the gap to the tip stays 6 px.

After: popovers on four sides, each with an arrow

What I did NOT exercise

  • No Studio surface opts in yet, so the captures come from a throwaway page, not from Studio itself.

@miguel-heygen
miguel-heygen marked this pull request as ready for review September 30, 2026 08:55

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arrow is really off by default.

  • No call site changes. Nothing in this repo renders <Popover> directly, since it's only exported through ui.ts and components/ui/index.ts. Every consumer of the exported component that I found passes side and align (plus open, onOpenChange and aria-label), and none passes arrow or sideOffset. So each one gets arrow = false, sideOffset = SIDE_OFFSET (6, same as before), and no BasePopover.Arrow in the tree.
  • The one unconditional change is inert. arrowPadding={ARROW_CORNER_CLEARANCE} on the Positioner only feeds Floating UI's arrow middleware, which returns {} when no arrow element is registered. So positioning is unchanged when arrow is unset.
  • The arrow isn't clipped. popupSurface (Menu.tsx:49) has no overflow-hidden, so the arrow's negative offset draws outside the popup the way it should.
  • The test checks both sides. "points at its trigger only when asked" fails if the arrow always renders (the svg is null by default) and also if it never renders.

Nit, not blocking: if a caller passes both arrow and a custom sideOffset, the offset doesn't grow by the arrow's height. The prop's doc says "the default gap", so that's documented. It's worth remembering when the first surface opts in.

Verdict: APPROVE
Reasoning: A small, opt-in change that leaves every existing popover rendering the same, with a test that fails when the default is flipped.

— Rames Jusso

@miguel-heygen
miguel-heygen added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 18a1b22 Sep 30, 2026
55 checks passed
@miguel-heygen
miguel-heygen deleted the studio/popover-arrow branch September 30, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants