A headless hover/focus reveal primitive. Gives a container a scoped trigger that reveals (or conceals) content inside it when the container is hovered or receives keyboard focus: the classic "row actions appear on hover" pattern. The reveal is CSS-only: no hover state lives in React and hovering never triggers a re-render. The caller authors no StyleX for the reveal itself; the hook hands out the container and content styles, and a nested container shadows its ancestor, so nested containers never leak hover/focus into one another. Accessible by construction: revealed content is visually hidden at rest with position and opacity (never display:none), so it stays mounted, keeps its place in the tab order, and is announced to assistive technology; it reveals on :focus-within so keyboard users see it when tabbing in, stays visible on touch (never gated behind hover on coarse pointers), and honors prefers-reduced-motion.
Configuration object for the reveal container. Optional.
options.isEnabled
boolean · optional · default true
When false the hook is inert: the container gets no styles and content getters return no styles, so content is always shown. Read on every render, so a component can flip it after mount (e.g. revealOn === "hover").
Spread onto the container whose hover/focus-within drives the reveal. Accepts hoverDelay (ms the pointer must dwell before the reveal starts: a hover-intent gate like Tooltip's and HoverCard's delay, so a cursor sweeping across a list leaves nothing painted behind it) and forceState ("active" | "inactive") to pin the trigger state when a caller owns it: a motion gate, a scroll, or a row whose menu is open. "inactive" still yields to keyboard focus and coarse pointers.
Spread onto each revealed / concealed child. Accepts isRevealInverted to conceal-on-hover instead of reveal-on-hover, isLayoutPreserved to reserve the layout box while hidden (opacity-only) and avoid layout shift, and forceVisibility ("shown" | "hidden") to pin this one element's appearance whatever the container is doing. "hidden" yields to focus.
Showcases and examples
1 documented example
useContainerReveal — Reveal-on-hover Row Actions
File rows keep their edit/delete actions hidden at rest and reveal them on hover or keyboard focus via useContainerReveal; the actions stay mounted and in the tab order.
Preview loads on approachPreview loads on approach
Exact source · use-container-reveal-hook-usage
// Copyright (c) Meta Platforms, Inc. and affiliates.
'use client';
import * as stylex from '@stylexjs/stylex';
import {useContainerReveal} from '@astryxdesign/core/hooks';
import {Button} from '@astryxdesign/core/Button';
import {Card} from '@astryxdesign/core/Card';
import {Icon} from '@astryxdesign/core/Icon';
import {Item} from '@astryxdesign/core/Item';
import {Stack} from '@astryxdesign/core/Layout';
import {Text} from '@astryxdesign/core/Text';
import {mergeProps} from '@astryxdesign/core/utils';
import {PencilIcon, TrashIcon} from '@heroicons/react/24/outline';
const styles = stylex.create({
actions: {
display: 'flex',
gap: 4,
},
intro: {
paddingInline: 12,
paddingBottom: 8,
},
});
function FileRow({name}: {name: string}) {
// Destructure the two prop getters. Spreading getContainerProps() on the row
// gives it a scoped hover/focus-within trigger; getContentRevealProps() hides
// the actions at rest and reveals them when the row is hovered or focused.
const {getContainerProps, getContentRevealProps} = useContainerReveal();
return (
<Item
label={name}
description="Edited 2 hours ago"
endContent={
<span
{...mergeProps(
getContentRevealProps(),
stylex.props(styles.actions),
)}>
<Button
label={`Edit ${name}`}
variant="ghost"
isIconOnly
icon={<Icon icon={PencilIcon} size="sm" />}
/>
<Button
label={`Delete ${name}`}
variant="ghost"
isIconOnly
icon={<Icon icon={TrashIcon} size="sm" />}
/>
</span>
}
{...getContainerProps()}
/>
);
}
export default function UseContainerRevealHookUsage() {
return (
<Card width={420} padding={2}>
<Stack gap={0}>
<Text type="supporting" color="secondary" xstyle={styles.intro}>
Hover a row — or Tab into it — to reveal its actions. On touch they
stay visible.
</Text>
<FileRow name="report.pdf" />
<FileRow name="budget.xlsx" />
<FileRow name="notes.txt" />
</Stack>
</Card>
);
}
Usage
A headless hover/focus reveal primitive. Gives a container a scoped trigger that reveals (or conceals) content inside it when the container is hovered or receives keyboard focus: the classic "row actions appear on hover" pattern. The reveal is CSS-only: no hover state lives in React and hovering never triggers a re-render. The caller authors no StyleX for the reveal itself; the hook hands out the container and content styles, and a nested container shadows its ancestor, so nested containers never leak hover/focus into one another. Accessible by construction: revealed content is visually hidden at rest with position and opacity (never display:none), so it stays mounted, keeps its place in the tab order, and is announced to assistive technology; it reveals on :focus-within so keyboard users see it when tabbing in, stays visible on touch (never gated behind hover on coarse pointers), and honors prefers-reduced-motion.
Destructure getContainerProps and getContentRevealProps; spread getContainerProps() on the container (via mergeProps with your own stylex.props) and getContentRevealProps() on the content to reveal.
Use for secondary affordances: reveal-on-hover row actions (edit/copy/remove on list or table rows) and overlay controls on a card or media tile (e.g. Thumbnail's remove button).
Gate the reveal with isEnabled when a consumer prop decides whether content is revealed on hover or always shown; it can change at any time.
Pass isLayoutPreserved for absolutely-positioned or overlay content to reserve its box and avoid layout shift when it appears.
Set a hoverDelay (100-250ms) on rows in a long list, so a cursor travelling across the list does not light up every row it passes; keyboard and touch still reveal immediately.
Reach for forceState when something other than the pointer owns the interaction (a drag, a scroll or motion gate, an open row menu), and forceVisibility when just one element should ignore the container.
Reach past the API into the hook's private custom properties (--_reveal-opacity and friends) to suppress a reveal; use forceState / forceVisibility, which survive a rename.
Use it to hide content that must always be discoverable; keep essential actions visible instead of gating them behind hover.