Headless Core — Overview
@andersseen/headless-components is the foundation tier of this stack (see
the root README’s “Package Roles”): pure TypeScript state machines with no
DOM rendering and no styles. It powers
@andersseen/web-components and
@andersseen/vanilla-components internally, and is also
published standalone for teams building their own design system on top of it.
- ✅ Pure TypeScript, works with any framework (Stencil, Angular, React, Vue, vanilla JS)
- ✅ Reactive state, accessibility, and keyboard navigation
- ❌ Does not render any DOM
- ❌ Does not include any styles
Install
Section titled “Install”pnpm add @andersseen/headless-componentsThe pattern
Section titled “The pattern”Every component factory follows the same shape: create*(config) returns an
object exposing a reactive state snapshot, a subscribe function,
actions (setters), queries (derived getters), one or more prop-getters
you spread onto your own markup, and event handlers you wire to DOM events.
import { createButton } from '@andersseen/headless-components';
const button = createButton({ disabled: false, onClick: e => console.log('clicked'),});
button.state; // readonly snapshot: { disabled, loading, type }button.getButtonProps(); // props to spread on your <button>button.actions.setLoading(true); // mutate → notifies subscribersbutton.queries.isDisabled(); // derived booleanbutton.handleClick(event); // event handler (guards disabled/loading)Method names vary per component (a dropdown has getTriggerProps() /
getMenuProps() / handleKeyDown(), tabs has getTabProps(id), etc.), but the
five buckets — state, subscribe, actions, queries, prop-getters +
handlers — are consistent across all of them. See each factory’s own page for
its exact shape.
Reactivity: subscribe
Section titled “Reactivity: subscribe”The core is reactive: subscribe(cb) fires on every state change and returns an
unsubscribe function. This is what makes one factory work in any framework
without an adapter — you bridge it to whatever re-render primitive your
framework has.
const unsubscribe = button.subscribe(state => { console.log('button state changed:', state);});// …laterunsubscribe();// React: bridge subscribe → a re-renderimport { useSyncExternalStore, useRef } from 'react';import { createButton } from '@andersseen/headless-components';
function useButton(config) { const ref = useRef(null); ref.current ??= createButton(config); const button = ref.current; useSyncExternalStore(button.subscribe, () => button.state); return button;}Live example
Section titled “Live example”This is the actual, unmodified return value of
createButton({ disabled: false }) called live on this page — nothing rendered,
just the plain object a UI layer would spread onto its own <button>:
Available factories
Section titled “Available factories”Seventeen component factories, each with its own page: full config, state,
actions/queries, prop-getters, and a live demo. Import from the package root or
a per-component subpath (@andersseen/headless-components/button).
| Factory | Page | Powers |
|---|---|---|
createButton | Button | Button |
createAccordion | Accordion | Accordion |
createTabs | Tabs | Tabs |
createDropdown | Dropdown | Dropdown |
createModal | Modal | Modal |
createDrawer | Drawer | Drawer |
createTooltip | Tooltip | Tooltip |
createToastManager | Toast | Toast |
createInput | Input | Input |
createSelect | Select | Select |
createAlert | Alert | Alert |
createNavbar | Navbar | Navbar |
createSidebar | Sidebar | Sidebar |
createBreadcrumb | Breadcrumb | Breadcrumb |
createCarousel | Carousel | Carousel |
createMenuList | Menu List | Menu List |
createContextMenu | Context Menu | Context Menu |
Plus createMenuSelection — shared
menu-disclosure logic factored out of Dropdown and Context Menu, covered on the
Primitives page rather than getting its own.
Primitives
Section titled “Primitives”Alongside the factories above, the package exports the low-level building blocks
they’re made of — StateStore, createMachine, createIdGenerator, Keys,
and the shared AriaAttributes/DataAttributes prop-getter types. See
Primitives for the full reference.
import { createStore } from '@andersseen/headless-components';
const store = createStore({ count: 0 });const stop = store.subscribe((state, prev) => { console.log(prev.count, '→', state.count);});store.setState({ count: 1 }); // logs "0 → 1"store.setState({ count: 1 }); // no-op: value unchanged, subscribers not calledstop();Usage with Stencil
Section titled “Usage with Stencil”import { Component, State, h } from '@stencil/core';import { createButton, type ButtonReturn,} from '@andersseen/headless-components/button';
@Component({ tag: 'my-button', shadow: true })export class MyButton { @State() private buttonLogic: ButtonReturn;
componentWillLoad() { this.buttonLogic = createButton({ onClick: e => this.handleClick(e) }); }
private handleClick = (e: MouseEvent) => console.log('Button clicked!');
render() { const props = this.buttonLogic.getButtonProps(); return ( <button {...props} onClick={e => this.buttonLogic.handleClick(e)}> <slot /> </button> ); }}Usage with Angular
Section titled “Usage with Angular”import { Component, OnInit } from '@angular/core';import { createButton, type ButtonReturn,} from '@andersseen/headless-components/button';
@Component({ selector: 'app-button', template: ` <button [attr.type]="props.type" [disabled]="props.disabled" (click)="button.handleClick($event)" > <ng-content></ng-content> </button> `,})export class ButtonComponent implements OnInit { button!: ButtonReturn; props: any;
ngOnInit() { this.button = createButton({ onClick: () => console.log('clicked') }); this.props = this.button.getButtonProps(); // re-read props on change: this.button.subscribe(() => (this.props = this.button.getButtonProps())); }}Next steps
Section titled “Next steps”Primitives, then any factory page above for its full reference.