EXEPERTAI LAB

Research alpha

Time Machine
EXEPERTAI LAB
GALLERY / COLLECTION
← Browse Astryx gallery
hooks·hook·@astryxdesign/core

useContainerReveal

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.

Open in Playground @astryxdesign/core/hooks

Signature

Call shape
useContainerReveal(options: UseContainerRevealOptions = {}): UseContainerRevealReturn
Parameters
NameType and behavior
optionsUseContainerRevealOptions · optional
Configuration object for the reveal container. Optional.
options.isEnabledboolean · 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").
Returns
NameType and behavior
getContainerProps(options?: ContainerRevealOptions) => {className?: string; style?: CSSProperties}
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.
getContentRevealProps(options?: ContentRevealOptions) => {className?: string; style?: CSSProperties}
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.