primitives
Headless behaviour managers: focus-scope, floating, roving-focus, virtualizer and more.
Layer 2 of the Fluixi headless UI ecosystem: reusable behavioral primitives that components compose. They contain no markup and render nothing.
Each primitive is a manager — a reactive function that receives an element
accessor, wires behavior (events, focus, registration, lifecycle), and returns a
disposer. Behavior is deliberately decoupled from rendering, so a primitive can
power any component regardless of how it is marked up. Managers run inside a
reactive owner and also self-clean via onCleanup.
Built on @fluixi/core (reactivity) and @fluixi/kit (Layer 1 utilities).
Overlay foundation
This release covers the primitives that the overlay components — Dialog, Popover, Tooltip, Menu, Select, Combobox, Drawer, HoverCard — are built from.
| Primitive | Import | Responsibility |
|---|---|---|
layerStack |
@fluixi/primitives/layer-stack |
global registry of open layers; topmost/nesting queries |
createEscapeKeyDown |
@fluixi/primitives/escape-key |
document-level Escape handling |
createInteractOutside |
@fluixi/primitives/outside-interaction |
pointer/focus interaction outside an element |
createDismissableLayer |
@fluixi/primitives/dismissable-layer |
composes the three above into layer-aware dismissal |
createPresence |
@fluixi/primitives/presence |
keeps an element mounted through its CSS exit animation |
lockScroll / createScrollLock |
@fluixi/primitives/scroll-lock |
reference-counted body scroll lock |
createFocusScope |
@fluixi/primitives/focus-scope |
focus trapping, auto-focus and focus restoration |
Navigation family
The primitives that power list and menu widgets — Menu, Listbox, Select, Combobox, Tabs, RadioGroup, Toolbar, Accordion.
| Primitive | Import | Responsibility |
|---|---|---|
getNavigationAction / getNextEnabledIndex |
@fluixi/primitives/keyboard-navigation |
pure key→action mapping and disabled-aware index math |
createCollection |
@fluixi/primitives/collection |
reactive registry of items kept in document order |
createRovingFocusGroup |
@fluixi/primitives/roving-focus |
roving tabindex + arrow navigation |
createTypeahead |
@fluixi/primitives/typeahead |
type-to-search buffer with ARIA conventions |
createSelectionManager |
@fluixi/primitives/selection-manager |
single/multiple selection state |
createRovingFocusGroup composes createCollection (ordering) with the pure
keyboard-navigation helpers (index math), and createSelectionManager builds
on @fluixi/kit's createControllableSignal so selection can be controlled or
uncontrolled with one API.
import { createRovingFocusGroup } from '@fluixi/primitives/roving-focus';
const group = createRovingFocusGroup({ orientation: () => 'vertical', loop: () => true });
// each item: group.collection.register({ ref: el }); tabindex = group.tabIndexFor(el)
// on the container: onKeyDown={group.onKeyDown}
Composition
createDismissableLayer is the worked example of composition over duplication:
it registers the element on the layerStack, then reuses createEscapeKeyDown
and createInteractOutside — dismissing only when the element is the topmost
layer, and never when a handler calls preventDefault(). The same building
blocks are independently useful: a Popover can use createInteractOutside
without full layer semantics; any widget can use createEscapeKeyDown.
import { createDismissableLayer } from '@fluixi/primitives/dismissable-layer';
createDismissableLayer(() => contentEl(), {
onDismiss: () => setOpen(false),
disableOutsidePointerEvents: true,
});
Interaction family
Pointer and keyboard interaction managers that attach to an element and expose reactive state.
| Primitive | Import | Responsibility |
|---|---|---|
createPress |
@fluixi/primitives/press |
unified pointer + keyboard press with isPressed state |
createHover |
@fluixi/primitives/hover |
fine-pointer hover tracking (ignores touch) |
createLongPress |
@fluixi/primitives/long-press |
threshold-based long press |
Observers, forms and positioning
| Primitive | Import | Responsibility |
|---|---|---|
createResizeObserver |
@fluixi/primitives/resize-observer |
reactive ResizeObserver binding |
createIntersectionObserver / createIsIntersecting |
@fluixi/primitives/intersection-observer |
reactive IntersectionObserver binding |
createFormControl |
@fluixi/primitives/form-control |
field id/label/description/error wiring and ARIA/validation state |
visuallyHiddenProps |
@fluixi/primitives/visually-hidden |
spreadable screen-reader-only styles |
Portal |
@fluixi/primitives/portal |
re-export of the framework Portal |
createFloatingPosition |
@fluixi/primitives/floating-layer |
reactive anchored positioning (wraps @floating-ui/dom) |
createFormControl is the wiring layer for fields — it generates ids, computes
aria-describedby from registered description/error elements, and exposes
validation state. It is intentionally independent of any form-state engine; the
component layer bridges external field state (e.g. @fluixi/forms) into it.
createFloatingPosition is the one primitive with an external dependency:
@floating-ui/dom, the standard positioning engine.
It keeps a floating element aligned to its reference via autoUpdate and exposes
the resolved coordinates, placement and arrow offsets as signals; applying them
to the DOM is left to the component.
Accessibility
These primitives implement the behavior WAI-ARIA dialog and popup patterns
require: focus is trapped within modal surfaces and restored to the trigger on
close, Escape dismisses the topmost layer, and outside interaction closes
transient surfaces. createPresence ensures exit animations complete before an
element leaves the accessibility tree.
Performance
- Side-effect free (
"sideEffects": false) with per-primitive subpath exports for precise tree-shaking. - Listeners are attached once per manager and removed on disposal; the layer stack is a single shared array rather than per-instance machinery.
- SSR-safe: scroll locking and DOM access no-op on the server.
Extension points
Every manager accepts an ownerDocument for multi-window / portal scenarios and
exposes granular callbacks (onEscapeKeyDown, onPointerDownOutside,
onFocusOutside) that can call preventDefault() to opt out of the default
behavior. Components layer their own semantics on top without forking the
primitive.
Part of the Fluixi UI component library. Made with ☕ by the Fluixi team.