Disclosure & OverlaysPopover

A floating chrome surface anchored to its trigger, on the Radix popover primitive.

Preview

The thinking

A popover is the cheapest way to put a small editor next to the thing it edits — the demo sets export dimensions without leaving the canvas. The load-bearing decision is what it refuses: no scrim, no focus trap, no stopping the page. A dialog interrupts the work to ask about it; this leans in beside the work and keeps it live, so leaving costs one click on whatever you wanted next. Everything else — the chrome white, the full hairline, the pop from its anchor's corner — exists so a surface with no claim on the page still reads as its own thing the instant it lands. The page never stops.

Interface design

The surface is bg-popover — in light mode that's the white reserved for interactive chrome, so floating UI always reads as machinery, never as page content. It gets the full border-border hairline rather than the card's faint one, because a surface hovering over arbitrary ground can't count on contrast to find its own edge. No shadow: depth here is fills and hairlines, and the pop motion does the rest of the announcing. Geometry is one size — w-72 wide, p-4 inside, rounded-lg, portalled above everything at z-50 — and it rests a sideOffset of 6 off its anchor, close enough to belong to it without sitting on it.

Interaction

Transform-origin comes from --radix-popover-content-transform-origin, so the surface scales out of whichever corner faces its anchor — surfaces grow out of what summoned them, and the origin survives a collision flip because Radix recomputes the var. The pop itself is opacity plus the standalone scale property from 0.96, on cubic-bezier(0.23, 0.88, 0.26, 0.92). It enters in 200ms and leaves in 150: an overlay should get out of the way faster than it arrived, since the dismissal is always in service of whatever you wanted underneath. The intent-reading is Radix's light dismiss — a click anywhere outside means you're done here, so the popover closes without asking, and the gesture that dismisses it is the same one that starts your next action. Leaving costs nothing.

Sound design

The popover ships no audio and marks nothing data-silent. The trigger speaks for it: the global SoundLayer taps anything carrying an interactive role through one capturing click listener, and the demo's trigger is a real button, so opening and closing from the trigger both tap — the sound tracks the click, not the popover's state. I didn't reach for the dialog's paired open and close either; that pair marks the page stopping, and a popover never stops the page. So dismissal is exactly as loud as what you leave for: click empty ground and nothing sounds, click a control and that control speaks.

Empathy

The semantics are Radix's, kept whole: the surface announces as a dialog, the trigger reports aria-haspopup and aria-expanded, focus moves into the panel when it opens, and Escape hands it back to the trigger. A click outside dismisses too, but focus stays with whatever you clicked — the light dismiss would be a lie if it yanked you back. The panel itself takes outline-none — it can receive focus on open, and a ring around an entire surface marks nothing; the controls inside draw their own. Under reduced motion, motion-reduce:animate-none drops the pop and the surface simply appears. Width is fixed at w-72, so long content wraps instead of reflowing the panel; there's no max-height and no internal scroll, and my position is that a popover needing a scrollbar has outgrown the pattern — at that size it should be a dialog or a page. Disabled isn't this component's problem to solve: the trigger renders whatever the demo composes into it, and a real button already knows how to be disabled.
popover.tsx
"use client";

import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";

import { cn } from "@/lib/utils";

const Popover = PopoverPrimitive.Root;

const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverAnchor = PopoverPrimitive.Anchor;

// transform-origin comes from the radix popper var, so the surface
// scales out of whichever corner faces its anchor — even after a
// collision flip
const PopoverContent = ({
	ref,
	className,
	align = "center",
	sideOffset = 6,
	...props
}: React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
	ref?: React.Ref<React.ComponentRef<typeof PopoverPrimitive.Content>>;
}) => (
	<PopoverPrimitive.Portal>
		<PopoverPrimitive.Content
			ref={ref}
			align={align}
			sideOffset={sideOffset}
			className={cn(
				"z-50 w-72 origin-[--radix-popover-content-transform-origin] rounded-lg border border-border bg-popover p-4 text-popover-foreground outline-none data-[state=open]:animate-pop-in data-[state=closed]:animate-pop-out motion-reduce:animate-none",
				className
			)}
			{...props}
		/>
	</PopoverPrimitive.Portal>
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };