Verge
A reserved slot whose controls rest hidden and reveal on hover, on focus, and always on touch — holding their space throughout, so nothing moves.
- Kiln-fired terracottaPoint at this row
- Brushed aluminiumOr tab into this one
- End-grain oakHeld open — what a touch screen renders
Install
npm install @patternmode/vergeimport { Verge } from "@patternmode/verge";
import "@patternmode/verge/styles.css";
export function FinishRow({ finish }) {
return (
<Verge.Root as="li">
<span>{finish.name}</span>
<Verge.Slot slots={2}>
<IconButton label="Rename" />
<OverflowMenu />
</Verge.Slot>
</Verge.Root>
);
}What it already handles
The reveal contract
Registries ship the button, the dropdown and the tooltip — the things being revealed. None ships the contract governing when they appear, which is where hand-rolled versions drift apart.
Keyboard- :focus-withinTabbing into the row reveals the slot, via :focus-within on the root. Children stay in the tab order at rest, which is what lets focus arrive at all. Spelling this focus-visible is the defect that recurs: it reveals the overflow menu two elements away but not the checkbox beside it.
Touch- @media (hover: none)No hover exists on a touch surface, so nothing hides. Handled with a media query, not a tap handler. Controls also take a wider gap on coarse pointers, because permanently-visible controls 2px apart are not separable by a thumb.
Open menus- [data-popup-open] | [data-state="open"] | [aria-expanded="true"]A slot holding an open menu, popover or disclosure stays revealed, so the overflow control does not vanish when the pointer travels to the menu it just opened.
Layout stability- opacity + pointer-eventsOnly opacity and pointer-events change. The slot occupies its space at rest and revealed alike, so the row's geometry never shifts under the pointer.
Nesting- inherited custom propertiesRoots may nest. A nested root shadows its ancestor's state for its own subtree, so pointing at an outer row does not reveal an inner row's controls.
Reduced motion- @media (prefers-reduced-motion: reduce)The transition is dropped under prefers-reduced-motion: reduce.
Core API
Verge.Root
The container whose hover and focus state the slots read. Wrap the whole row, not just the controls — the reveal responds to the user addressing the row. Contributes no appearance of its own.
as- ElementType"div"Element or component to render as, so a row inside a list can be an li and a cell can be a td.
Verge.Slot
The reserved area holding the controls.
slots- numberHow many controls' worth of width to hold open. Only needed when controls must align into a column across rows carrying different numbers of them. Width per control comes from --patternmode-verge-slot-size.
visible- booleanfalseKeeps the slot revealed regardless of pointer or focus, for state the CSS cannot see such as an action mid-flight. Not an escape hatch for one row mattering more — a control drawn on every row at rest is furniture.
as- ElementType"div"Element or component to render as.
What you must handle: touch hit areas
/* Verge reserves the slot; it does not own your button. On touch the
controls are permanently visible, so each needs a 48x48px hit area. */
@custom-variant pointer-fine (@media (pointer: fine));Theming
Custom properties
Every knob is a custom property, so retune without forking.
--patternmode-verge-slot-size- length1.75remWidth one control reserves, used by slots.
--patternmode-verge-duration- time120msReveal transition duration.
--patternmode-verge-easing- easing-functioncubic-bezier(0.2, 0, 0, 1)Reveal transition easing.