All components
Hover Card
Rich preview panels revealed from a trigger, in four padding sizes.
123456789101112131415161718192021import { HoverCard } from "#components/ui/ark-ui/hover-card.tsx";
export function Example() { return ( // size resolves once on Root; the positioner portals to <body> and the // machine handles hover intent with open/close delays. <HoverCard.Root size="md" openDelay={200} closeDelay={300}> <HoverCard.Trigger asChild> <a href="https://x.com/stylexblocks">@stylexblocks</a> </HoverCard.Trigger> <HoverCard.Portal> <HoverCard.Positioner> <HoverCard.Content> <strong>StyleX Blocks</strong> <p>Production-ready blocks and themes, compiled with StyleX.</p> </HoverCard.Content> </HoverCard.Positioner> </HoverCard.Portal> </HoverCard.Root> );}Sizes
Install
1npx stylexblocks add hover-cardSource
components/ui/ark-ui/hover-card.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126import * as stylex from "@stylexjs/stylex";import { HoverCard as ArkHoverCard } from "@ark-ui/react/hover-card";import { Portal } from "@ark-ui/react/portal";import { createSlotRecipeContext } from "#lib/ark-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 — Ark's hover-card machine wearing the hover-card slot recipe: * real portal to `document.body`, hover intent with open/close delays, and * anchor positioning all come from the machine. `<HoverCard.Root size>` * resolves the recipe once; parts inherit it through context and compose via * `asChild`. Portal and Positioner stay unstyled engine parts. */export const HoverCard = { Root: withRootProvider(ArkHoverCard.Root, "HoverCardRoot"), Trigger: ArkHoverCard.Trigger, Context: ArkHoverCard.Context, Portal, Positioner: ArkHoverCard.Positioner, Content: withContext( ArkHoverCard.Content, (v) => [...resolveSlotStyles(hoverCardRecipe.slots.content, v), chrome.content], "HoverCardContent", ), Arrow: withContext(ArkHoverCard.Arrow, "arrow", "HoverCardArrow"), ArrowTip: withContext(ArkHoverCard.ArrowTip, "arrowTip", "HoverCardArrowTip"),};