All components
Popover
Anchored panels with header, body, and footer slots in four padding sizes.
123456789101112131415161718192021222324252627import { Popover } from "#components/ui/base-ui/popover.tsx";import { Button } from "#components/ui/base-ui/button.tsx";
export function Example() { return ( // size resolves once on Root; parts inherit it via context. Portal → // Positioner → Popup is Base UI's real overlay chain. <Popover.Root size="md"> <Popover.Trigger render={<Button variant="surface" />}> Share settings </Popover.Trigger> <Popover.Portal> <Popover.Positioner sideOffset={8}> <Popover.Popup> <Popover.Title>Share this block</Popover.Title> <Popover.Description> Anyone with the link can view this block. </Popover.Description> <Popover.Footer> <Button variant="solid" size="sm">Copy link</Button> </Popover.Footer> </Popover.Popup> </Popover.Positioner> </Popover.Portal> </Popover.Root> );}Sizes
xs
sm
md
lg
Install
1npx stylexblocks add popoverSource
components/ui/base-ui/popover.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155import * as stylex from "@stylexjs/stylex";import { Popover as BasePopover } from "@base-ui-components/react/popover";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles } from "#lib/recipe-props.ts";import { semanticShadows } from "@stylexblocks/ui/semantic-tokens/shadows.stylex.ts";import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const popoverContent = stylex.create({ base: { position: 'relative', display: 'flex', flexDirection: 'column', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, '--popover-bg': semanticColors.bgPanel, backgroundColor: 'var(--popover-bg)', '--popover-size': sizes.xs, '--popover-mobile-size': 'calc(100dvw - 1rem)', '--popover-z-index': zIndices.layer2, zIndex: 'calc(var(--popover-z-index) + var(--layer-index, 0))', outline: 'none', '--popover-radius': semanticRadii.panel, '--popover-concentric-radius': 'max(0px, calc(var(--popover-radius) - var(--popover-padding)))', transformOrigin: 'var(--transform-origin)', animationName: { default: null, ':is([data-open], [data-state="open"])': 'scale-in, fade-in', ':is([data-closed], [data-state="closed"])': 'scale-out, fade-out', }, animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.fast, ':is([data-closed], [data-state="closed"])': durations.faster, }, },})
const popoverContentSizes = stylex.create({ xs: { '--popover-padding': spacing._3, }, sm: { '--popover-padding': spacing._4, }, md: { '--popover-padding': spacing._5, }, lg: { '--popover-padding': spacing._6, },})
const popoverHeader = stylex.create({ base: { paddingInline: 'var(--popover-padding)', paddingTop: 'var(--popover-padding)', },})
const popoverBody = stylex.create({ base: { padding: 'var(--popover-padding)', flex: 1, },})
const popoverFooter = stylex.create({ base: { display: 'flex', alignItems: 'center', paddingInline: 'var(--popover-padding)', paddingBottom: 'var(--popover-padding)', },})
const popoverArrow = stylex.create({ base: { '--arrow-size': sizes._3, '--arrow-background': 'var(--popover-bg)', },})
const popoverArrowTip = stylex.create({ base: { borderTopWidth: '1px', borderInlineStartWidth: '1px', },})
export type PopoverSize = keyof typeof popoverContentSizes
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axisconst popoverRecipe = defineSlotRecipe({ slots: { content: { styles: popoverContent, sizes: popoverContentSizes }, header: { styles: popoverHeader }, body: { styles: popoverBody }, footer: { styles: popoverFooter }, arrow: { styles: popoverArrow }, arrowTip: { styles: popoverArrowTip }, }, defaultVariants: { size: "md" },});
// Local chrome the recipe leaves to the composition layer: the panel surface// and a z-index that clears sticky page chrome — the generated `layer2` (10)// would sit beneath a `z-index: 50` site header.const chrome = stylex.create({ content: { "--popover-z-index": zIndices.popover, width: "var(--popover-size)", maxWidth: "var(--popover-mobile-size)", borderRadius: "var(--popover-radius)", boxShadow: semanticShadows.overlay, },});
const { withRootProvider, withContext } = createSlotRecipeContext(popoverRecipe);
/** * Popover — Base UI's popover machine wearing the popover slot recipe: real * portal (via `Popover.Portal`), anchor positioning, focus management, and * dismiss behavior all come from the engine's Portal → Positioner → Popup * chain. `<Popover.Root size>` resolves the recipe once; every part inherits * it through context and composes via `render`. Portal and Positioner stay * unstyled engine parts. */export const Popover = { Root: withRootProvider(BasePopover.Root, "PopoverRoot"), Trigger: BasePopover.Trigger, Portal: BasePopover.Portal, Positioner: BasePopover.Positioner, Popup: withContext( BasePopover.Popup, (v) => [...resolveSlotStyles(popoverRecipe.slots.content, v), chrome.content], "PopoverPopup", ), Header: withContext("div", "header", "PopoverHeader"), Body: withContext("div", "body", "PopoverBody"), Footer: withContext("div", "footer", "PopoverFooter"), Title: withContext(BasePopover.Title, "header", "PopoverTitle"), Description: withContext(BasePopover.Description, "body", "PopoverDescription"), Close: BasePopover.Close, Arrow: withContext(BasePopover.Arrow, "arrow", "PopoverArrow"),};