Back to Onyx

OpenButton

web/lib/opal/src/components/buttons/open-button/README.md

3.3.04.3 KB
Original Source

OpenButton

Import: import { OpenButton, type OpenButtonProps } from "@opal/components";

A trigger button with a built-in chevron that rotates when open. Hardcodes variant="select-heavy" and delegates to Interactive.Stateful, adding automatic open-state detection from Radix data-state. Designed to work automatically with Radix primitives while also supporting explicit control via the interaction prop.

Relationship to SelectButton

OpenButton is structurally near-identical to SelectButton — both share the same call stack:

Interactive.Stateful → Interactive.Container → content row (icon + label + trailing icon)

OpenButton is a tighter, specialized use-case of SelectButton:

  • It hardcodes variant="select-heavy" (SelectButton exposes variant)
  • It adds a built-in chevron with CSS-driven rotation (SelectButton has no chevron)
  • It auto-detects Radix data-state="open" to derive interaction (SelectButton has no Radix awareness)
  • It does not support rightIcon (SelectButton does)

Both components support foldable using the same pattern: interactive-foldable-host class + Interactive.Foldable wrapper around the label and trailing icon. When foldable, the left icon stays visible while the rest collapses. If you change the foldable implementation in one, update the other to match.

If you need a general-purpose stateful toggle, use SelectButton. If you need a popover/dropdown trigger with a chevron, use OpenButton.

Architecture

Interactive.Stateful           <- variant="select-heavy", interaction, state, disabled, onClick
  └─ Interactive.Container     <- height, rounding, padding (from `size`)
       └─ div.opal-button.interactive-foreground [.interactive-foldable-host]
            ├─ div > Icon?                 (interactive-foreground-icon)
            ├─ [Foldable]?                 (wraps label + chevron when foldable)
            │    ├─ <span>?                .opal-button-label
            │    └─ div > ChevronIcon      .opal-open-button-chevron
            └─ <span>? / ChevronIcon       (non-foldable)
  • interaction controls both the chevron and the hover visual state. When interaction is "hover" (explicitly or via Radix data-state="open"), the chevron rotates 180° and the hover background activates.
  • Open-state detection is dual-resolution: the explicit interaction prop takes priority; otherwise the component reads data-state="open" injected by Radix triggers (e.g. Popover.Trigger).
  • Chevron rotation is CSS-driven via .interactive[data-interaction="hover"] .opal-open-button-chevron { rotate: -180deg }. The ChevronIcon is a stable named component (not an inline function) to preserve React element identity across renders.

Props

PropTypeDefaultDescription
state"empty" | "filled" | "selected""empty"Current value state
interaction"rest" | "hover" | "active"autoJS-controlled interaction override. Falls back to Radix data-state="open" when omitted.
iconIconFunctionComponentLeft icon component
childrenstringContent between icon and chevron
foldablebooleanfalseWhen true, requires both icon and children; the left icon stays visible while the label + chevron collapse when not hovered. If tooltip is omitted on a disabled foldable button, the label text is used as the tooltip.
sizeSizeVariant"lg"Size preset controlling height, rounding, and padding
widthWidthVariantWidth preset
tooltipstringTooltip text shown on hover
tooltipSideTooltipSide"top"Which side the tooltip appears on
disabledbooleanfalseDisables the button

Usage

tsx
import { OpenButton } from "@opal/components";
import { SvgFilter } from "@opal/icons";

// Basic usage with Radix Popover (auto-detects open state)
<Popover.Trigger asChild>
  <OpenButton>Select option</OpenButton>
</Popover.Trigger>

// Explicit interaction control
<OpenButton interaction={isExpanded ? "hover" : "rest"} onClick={toggle}>
  Advanced settings
</OpenButton>

// With left icon
<OpenButton icon={SvgFilter} state="filled">
  Filters
</OpenButton>