HelixOverviewComponentsMapsGraphIntroductionInstallationDeriving a themeSandboxTheme builderVisual testingCortexDocsEngramBuildingTraceBuildingRecallDeclaredServicesInsightsGet in touchSource on GitHub
Essentials
Building on the headless tierThe headless tier is a set of components with no stylesheet. Each component
provides:structureARIAkeyboard behaviorstateform participationNone of it sets how the component looks. You can use the headless tier as the
foundation of your own design system, with your own tag names and your own
stylesheets.Theming Helix keeps our components and changes their tokens. Building on the
headless tier keeps our behavior and replaces the whole presentation layer,
under names of your own.Set your own tag prefixEvery headless component renders its children with an interpolated tag name.
The prefix of that name comes from a static on the class:import{HelixHeadlessCombobox}from'@fusion.dev/helix-headless/combobox'exportclassAcmeComboboxextendsHelixHeadlessCombobox{staticoverridetier='acme-'}customElements.define('acme-combobox',AcmeCombobox)That one line sets the prefix. The styled tier sets its prefix the same way, with
static override tier = 'helix-' and nothing else. Every element in the
headless manifest carries the static, so you can find it there.Register every component a component rendersWhen a component renders another component, register both.If you register acme-combobox without acme-listbox, the panel renders a tag
that is not defined. An unregistered custom element is an inert, unstyled inline
box, so the panel shows every option with no list around them, and typing
filters nothing. In development, the combobox says so once in the console and
names the tag to define.43 of the 108 headless families interpolate another family's tag, across 26 distinct children.FamilyRendersarea-chart, bar-chart, copy-button, donut-chart, heatmap, line-chart, scatter-chart, spatial-bar-chart, spatial-heatmap, spatial-ribbon-chart, spatial-scatter-chart, spatial-surface-chart, spatial-treemaptooltipbreadcrumbsbreadcrumbcanvas, device-screen, graph, mapicon-buttonchatchat-messagecheckbox-group, tablecheckboxcode-block, object-inspectorcopy-buttoncolor-inputcolor-pickercolor-pickerbutton, button-group, slidercomboboxlistbox, tagcommanddialog, listboxdata-tablecheckbox, paginationdate-inputdate-picker, tagfile-inputtagfilter-inputdivider, menu, menu-itemloglog-group, log-linemenukbdnotificationsformat-datepaginationoption, selectselectlistboxsteps, tourbuttonterminalbutton, icon-button, toolbartime-inputtag, time-pickertime-pickerlistbox, optionvideo, video-playlistmenu, menu-itemSubclass and register each family together with everything it renders, and
everything those families render in turn. For example, command reaches
dialog and listbox.The elements you write inside a component are yours to register as well.
A select renders none of the options you write in it, so acme-select needs
your acme-option registered beside it. In development, a component holding a
child of its tier that nobody registered says so once and names the tag to
define.
A listbox given items and a filter input given a source draw options of
your tier from data, and those need acme-option registered too.We generate the table from the manifest, which publishes the graph as
helix.renders on every element that interpolates a tag. We kept the table by
hand until September 22, 2026. By then, four families in it were wrong. The
generated table is always current.What you write yourselfA :not(:defined) rule@fusion.dev/helix-styles ships an elements.css that hides each of its tags until
the tag's definition loads. Your page then does not show unstyled text that
changes into components. The headless package ships no such file, and no tool
can generate one for tags you have not named yet. Write the rule for your own
elements:acme-combobox:not(:defined),acme-listbox:not(:defined){visibility:hidden;}The rule hides each element until its definition loads, and the layout
settles when the elements upgrade.A rule that hides the live partsA live part is a live region. It holds words only a screen reader should
read, such as how many options a filter left, and the headless tier draws it
as ordinary text. Hide each one visually with a clip, which keeps it in the
accessibility tree:acme-combobox::part(live),acme-combobox::part(listbox-live){block-size:1px;clip-path:inset(50%);inline-size:1px;overflow:hidden;position:absolute;white-space:nowrap;}display: none and visibility: hidden take a region out of the
accessibility tree as well, and a screen reader then hears nothing. A
component forwards the live part of a component it renders under a name of
its own, such as listbox-live on Combobox. The styled tier hides every one
of them.Typings for your tagsYour typings come from a custom elements manifest of your package, written by
@custom-elements-manifest/analyzer:npx @custom-elements-manifest/analyzer analyze --litelement--dependencies \
--globs"src/**/*.ts"Each flag does one job, and the manifest is incomplete without either:--dependencies reads the custom-elements.json each Helix package ships
and names in its package.json, so acme-combobox gets the members,
attributes, events, slots and parts it inherits from ours.--litelement reads Lit's decorators, so the properties you declare with
@property are attributes too.Then run a typings generator over the manifest. Our own typings start from
@wc-toolkit/jsx-types and @wc-toolkit/vuejs-types. The typings for
acme-combobox then include the members it inherits, as well as the ones you
wrote.The generated file names Helix's event classes, such as
HelixBeforeChangeEvent, without importing them. Add one line at its top that
imports the names TypeScript reports as missing:importtype{HelixBeforeChangeEvent,HelixFilterEvent}from'@fusion.dev/helix-core'Without that line, a project that sets skipLibCheck gets no error, and each
of those event handlers is typed any.For a few tags in React, you can skip the generator. Each headless family
publishes its props in @fusion.dev/helix-headless/jsx, and your tag takes
them:importtype{HelixHeadlessComboboxProps}from'@fusion.dev/helix-headless/jsx'declaremodule'react'{namespaceJSX{interfaceIntrinsicElements{'acme-combobox':HelixHeadlessComboboxProps}}}A subclass that adds properties joins its own to them with &.@fusion.dev/helix-cem, the step that runs those generators for our packages
and corrects what they write, is not published. It is build tooling for this
repository.The tag mapWrite the tag map yourself, with one line per element, beside the class:declareglobal{interfaceHTMLElementTagNameMap{'acme-combobox':AcmeCombobox}}TypeScript declares custom elements this way. The interface merges from your
source, and a generated file cannot name a class that is not in the same
program.Check your components with helix componenthelix component checks the components you write on Helix's base classes. Name
the folders that hold them in helix.json at your project's root:{"tiers":["src/components"]}Then run it:helix component audit
helix component checkaudit prints each rule and what it found, and check prints nothing when
there is nothing to report. Both exit 1 on a finding, and --json prints the
findings as data. The rules check what your components document and write:RuleWhat it checkstierA component built on a headless one that renders children sets tier, so its children are yours and not Helix'sregisteredEvery element a component's base renders is registered under the prefix it setscomposedA module that renders one of your elements, by its base or in its own template, imports the module that registers itslotsA @slot you document is one the Helix class under it takes, or one your template rendersexportpartsA ::part() written at your elements or at Helix's names a part that element publisheswordsA component with static words declares its English defaults @words, and joins no word into a sentence outside the messagedispatchedAn event your component sends by name, with new CustomEvent or new Event, is documented with @eventpartsA @csspart you document is rendered, and a part you render is documentedstatesA custom state you record is documented with @cssstate, and a documented one is recordedhooksA @cssproperty you document is read, and one you read is documentedrestatedA part, a slot, or an event is declared oncereflectedA property that reflects has no default, so a host carries no attribute nobody wrotereflected-defaultsA reflected property writes no attribute on upgrade that a server did not sendattribute-namesA property that mirrors a platform attribute answers to the platform's spellingconstructedNo class field reads the element's attributes in its initializer, where a parsed element has none yetobserversAn observer built once is not torn down by a lifecycle that runs many timesleaksA listener on something the element does not own is removed when the element is--only parts,states runs a few of them.What the headless tier handles for youTree-shaking@fusion.dev/helix-headless declares "sideEffects": true, because its
modules register elements when you import them. A bundler that treated them as
free of side effects would drop a registration, and a tag would never upgrade.
Import each family from its own entry, such as
@fusion.dev/helix-headless/combobox, so your bundle carries only the families
you use.Documented partsEvery part has an @csspart entry, so every ::part() you can use is
published. You write your presentation layer against these named parts, and it
does not depend on our internal markup.BehaviorThese features live below the tier you replace, so they keep working under your
tag names:focus managementtypeaheadthe roving tab stopform participationvaliditytier is not yet a versioned APItier is an implementation detail that is visible in the manifest. Using it to
set your own prefix makes it an API that needs a stability contract, and we
have not written that contract yet. The pattern is supported but not yet
versioned, so pin your Helix version.Building on the headless tier compared with white-labelingBuilding on the headless tier fits you if you are building a design system on
top of ours and our tags can appear beside yours.White-labeling fits a customer who wants Helix shipped under their own name,
with no Helix tags. It costs more, and it needs:the tag literals rewritten at build timea second distribution to publishan artifact per customerBuilding on the headless tier needs none of these.You own the presentation layer, the tags, and the build. If we stopped
tomorrow, you would keep a working design system. You would lose our fixes to
the behavior underneath it.