All components
Hover Card
Rich preview panels revealed from a trigger, in four padding sizes.
1234567891011121314151617181920import { HoverCard } from "#components/ui/base-ui/hover-card.tsx";
// Base UI's PreviewCard machine wearing the hover-card recipe.export function Example() { return ( <HoverCard.Root size="md"> <HoverCard.Trigger href="https://x.com/stylexblocks"> @stylexblocks </HoverCard.Trigger> <HoverCard.Portal> <HoverCard.Positioner sideOffset={8}> <HoverCard.Popup> <strong>StyleX Blocks</strong> <p>Production-ready blocks and themes, compiled with StyleX.</p> </HoverCard.Popup> </HoverCard.Positioner> </HoverCard.Portal> </HoverCard.Root> );}Sizes
Install
1npx stylexblocks add hover-cardSource
components/ui/base-ui/hover-card.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124import * as stylex from "@stylexjs/stylex";import { PreviewCard } from "@base-ui-components/react/preview-card";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 hoverCardContent = stylex.create({ base: { position: 'relative', display: 'flex', flexDirection: 'column', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, '--hovercard-bg': semanticColors.bgPanel, backgroundColor: 'var(--hovercard-bg)', maxWidth: sizes._80, '--hover-card-z-index': zIndices.layer2, zIndex: 'calc(var(--hover-card-z-index) + var(--layer-index, 0))', transformOrigin: 'var(--transform-origin)', outline: 'none', '--hover-card-radius': semanticRadii.panel, '--hover-card-concentric-radius': 'max(0px, calc(var(--hover-card-radius) - var(--hover-card-padding)))', 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 hoverCardContentSizes = stylex.create({ xs: { '--hover-card-padding': spacing._3, padding: 'var(--hover-card-padding)', }, sm: { '--hover-card-padding': spacing._4, padding: 'var(--hover-card-padding)', }, md: { '--hover-card-padding': spacing._5, padding: 'var(--hover-card-padding)', }, lg: { '--hover-card-padding': spacing._6, padding: 'var(--hover-card-padding)', },})
const hoverCardArrow = stylex.create({ base: { '--arrow-size': sizes._3, '--arrow-background': 'var(--hovercard-bg)', },})
const hoverCardArrowTip = stylex.create({ base: { borderTopWidth: '0.5px', borderInlineStartWidth: '0.5px', },})
export type HoverCardSize = keyof typeof hoverCardContentSizes
// 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 hoverCardRecipe = defineSlotRecipe({ slots: { content: { styles: hoverCardContent, sizes: hoverCardContentSizes }, arrow: { styles: hoverCardArrow }, arrowTip: { styles: hoverCardArrowTip }, }, 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: { "--hover-card-z-index": zIndices.popover, borderRadius: "var(--hover-card-radius)", boxShadow: semanticShadows.overlay, },});
const { withRootProvider, withContext } = createSlotRecipeContext(hoverCardRecipe);
/** * Hover Card — Base UI's preview-card machine wearing the hover-card slot * recipe: real portal (via `HoverCard.Portal`), hover intent with delays, * and anchor positioning all come from the engine's Portal → Positioner → * Popup chain. `<HoverCard.Root size>` resolves the recipe once; parts * inherit it through context and compose via `render`. Portal and Positioner * stay unstyled engine parts. */export const HoverCard = { Root: withRootProvider(PreviewCard.Root, "HoverCardRoot"), Trigger: PreviewCard.Trigger, Portal: PreviewCard.Portal, Positioner: PreviewCard.Positioner, Popup: withContext( PreviewCard.Popup, (v) => [...resolveSlotStyles(hoverCardRecipe.slots.content, v), chrome.content], "HoverCardPopup", ), Arrow: withContext(PreviewCard.Arrow, "arrow", "HoverCardArrow"),};