HelixOverviewComponentsMapsGraphIntroductionInstallationDeriving a themeSandboxTheme builderVisual testingCortexDocsEngramBuildingTraceBuildingRecallDeclaredServicesInsightsGet in touchSource on GitHub
Getting started
TiersHelix is four packages stacked on each other. Each package has one job, and each
depends only on the packages below it.PackageOwnsDepends on@fusion.dev/helix-stylesTokens and the base layer. Pure CSSnothing@fusion.dev/helix-coreElement base class, mixins, decoratorsLit@fusion.dev/helix-headlessStructure, accessibility, state, form participationcore@fusion.dev/helixPresentation: tokens, parts, and the visual axesheadless, styles@fusion.dev/helix-theme and @fusion.dev/helix-icons ship beside the stack.
Neither is a tier, and you do not need either one to render a component.There is no React package. We generate the framework typings into
@fusion.dev/helix and @fusion.dev/helix-headless, and the bridge that
repairs React's hydration of custom events is in @fusion.dev/helix-core. See React.Why the tiers are separate packagesheadless must never depend on styles.This rule lets you use the headless tier on its own. You install it and write
all of the styling yourself. Nothing about how Helix looks comes with it. If
styling moved down into the headless tier, the four packages would be one
package split across four files.What each tier containsThe headless tier owns what a component is and does:markup structureARIA wiringslotseventsinternal stateform participationIt ships no CSS, no token references, and none of the visual axes.The styled tier extends the headless tier and adds only presentation. Token
references live in the styled tier.// headless/src/accordion/item/accordion-item.ts// no CSS, no tokensexportclassHelixHeadlessAccordionItemextendsMixinControllerOpen(HelixElement){protectedoverriderender(){returnhtml` <div part="container"> <button aria-expanded=${this.open} part="trigger" @click=${this.handleToggle}> <slot name="summary"></slot> </button> </div> `}}// styled/src/accordion/root/accordion-root.ts// extends the headless one, adds presentation onlyimport{styles}from'./accordion-root.css.js'@customElement('helix-accordion')exportclassHelixAccordionextendsHelixHeadlessAccordion{@property({reflect:true})separation?:'outlined'|'plain'staticoverridestyles:CSSResultGroup=styles}/* styled/src/accordion/item/accordion-item.css */[part='trigger']{min-block-size:var(--helix-size-control);padding:var(--helix-space-sm);}Style a component from your stylesheetElements inside the shadow DOM carry no class names for you to select. Each
structural element carries a part, and parts are the public API for overriding
styles:helix-accordion-item::part(trigger){font-weight:600;}To change one value, set the component's own custom property. It defaults to a
system token. Setting it restyles that one component and leaves --helix-* and
every other component unchanged:helix-accordion{--accordion-radius:1rem;}Hide elements until they upgradeA custom element is an inline, unstyled box until its definition loads. Its
light-DOM children render immediately, in document order. A card first appears
as unstyled text and then changes into a card. The delay is short. It is most
visible on a server-rendered page, where the markup arrives long before the
JavaScript.@fusion.dev/helix-styles includes elements.css, which hides each tag of the
styled tier until the tag's definition loads:helix-card:not(:defined){visibility:hidden;}The headless tier ships no such file on purpose. There are three reasons, and
the third one affects you most.First, hiding an element is a design choice. Below the styled tier, we ship only
the rules a component cannot work without. For example, overflow creates a
scrollport, and [hidden] is the platform's own semantics. Hiding an element
that has not upgraded is a choice between two imperfect first frames. The
content is in the markup either way, so a crawler reads it in both cases.Second, the rule belongs to the document. A component cannot apply it from
inside, because there is no shadow root before the upgrade. The rule has to
live in your stylesheet, so the choice is yours.Third, one file for every tag would not fit your project. You import only the
headless elements you use, so a generated list would include tags you never ship.
Write the rule for the elements you have:helix-headless-accordion:not(:defined),helix-headless-dialog:not(:defined){visibility:hidden;}Use visibility, so a hidden element keeps the size of its unstyled content.
The layout still settles when the element upgrades and takes its own size.
display: none collapses the box, so the element takes no space until the
definition arrives, and then everything below it shifts by the element's full
size.You may want to hide nothing. If your first paint matters more than the final
layout, or your page has to be readable with JavaScript turned off, let the
unstyled markup show. Because of that choice, we do not write the file for you.Mixins and toolingopen and defaultOpen come from MixinControllerOpen in
@fusion.dev/helix-core. The manifest analyzer cannot follow a mixin into
another package. Document any API that a component gets from a mixin on the
component itself, with @event, @property, or a similar tag. Mixin API
without that documentation is missing from IDE tooltips, from the React wrapper
generator, and from the manifest that agents read.