HelixOverviewComponentsMapsGraphIntroductionInstallationDeriving a themeSandboxTheme builderVisual testingCortexDocsEngramBuildingTraceBuildingRecallDeclaredServicesInsightsGet in touchSource on GitHub
Essentials
CompositionHelix components come in families, parts, and groups. Each kind has a different
purpose. When you know which kind you are looking at, you know what to import
and what to write.A namespace means one component goes inside anotherTabs.Root contains Tabs.Tab. A namespace means the part goes inside the
family's root.A namespace never marks a version of a component. An icon-only button is its
own component, so IconButton is top-level. One rule decides this:Variants are props. Alternatives are separate components.A filled button and an outlined button are the same component with a different
emphasis. A button that holds only an icon has different requirements for its
accessible name and a different shape, so it is its own component.When a region is an element and when it is a slotA region is an element when it has behavior, state, or an API. Otherwise it is a
slot.Card's header, body, and footer had none of the three. Five elements became
one, and a title became a real <h2> that you write. Tabs.Tab,
Select.Option, and Accordion.Item have state, so they stay elements.An empty wrapper element still takes up layout space. A <slot> is
display: contents, so an unassigned slot generates no box. It adds no grid
row, no flex item, and no gap. A wrapper generates a box either way. We measured
Card with identical content. With empty wrappers present, it rendered at 85px,
and the ideal is 53px. With bare slots, it rendered at 53px with no hiding logic
at all.When a group is a part of a familyA group is a part of a family when it reports a form value.Radio.Group and Toggle.Group report a form value. They own the name, the
value, and the validity. A single radio is an answer, and the group is the
control it answers. The family is about the group, so the group is a part of the
family, and the member belongs nowhere else.Checkbox Group and Button Group do not report a form value. Neither calls
setFormValue. Each checkbox is form-associated and submits itself. The group
adds role="group", a shared name, and one place to read the set. A button
needs no group at all. Both groups are their own families, because their members
are complete controls without the group.A container that holds mixed content is its own family for the same reason.
Toolbar holds buttons, dropdowns, and inputs, so it is not a part of any of
their families.A content property is the fallback for its slotA property that holds content, such as a tooltip's text, is the fallback content
for a slot of the same name. Helix never renders both.<!-- The trigger, which names the tooltip in aria-describedby --><helix-buttonaria-describedby="save-tip"id="save">Save</helix-button><!-- The property --><helix-tooltipcontent="Save"for="save"id="save-tip"></helix-tooltip><!-- The slot, which replaces it --><helix-tooltipfor="save"id="save-tip"><spanslot="content"><strong>Save</strong>, or press <helix-kbd>⌘S</helix-kbd></span></helix-tooltip>Text stays an attribute, because most tooltips and most avatars hold only text.
To use richer content, put it in the slot, and it replaces the attribute. The
two never show at once, so no component has to check which one you used.A text node that holds only whitespace is assigned to the default slot and hides
its fallback. A newline between the tags is enough. For this reason, some
examples put the closing tag on the same line as the opening tag.Set a button's width with the layoutA button has no fill property and no size property. How wide a control is
depends on the region it is in. width="full" fills the row a button sits in,
and the layout decides everything else.A button already stretches in a column flex or a grid, because a stretch-aligned
parent stretches its children:<helix-flexdirection="column"><helix-buttonemphasis="solid"tone="primary">Fills its column</helix-button></helix-flex>To make a button only as wide as its label, set that on the layout:.hug{align-self:start;}<helix-buttonclass="hug">Hugs its label</helix-button>This default can surprise you. A button in a column is full width, and you might
look for a property to turn that off. The layout controls it.A fixed width belongs to your site's styles. A marketing page whose hero
buttons all share one width is a common and reusable practice. The number is
part of that page's design, so it does not belong to the button component. Name
it once, where your other page constants are defined::root{--site-cta-inline-size:13rem;}
.heroActionshelix-button{min-inline-size:var(--site-cta-inline-size);}For the same reason, a hero's type size is outside the component scale. A
marketing hero is not a component, so its measurements belong to your site.Name a field control with label or aria-labelEvery field control is named the same way, and there are two cases:<helix-inputlabel="Files"></helix-input><!-- a visible label --><helix-inputaria-label="Files"></helix-input><!-- named, nothing drawn --><helix-input></helix-input><!-- unnamed. avoid -->label gives the control a visible label. aria-label gives it a name and
draws nothing. With neither, a screen reader announces the control as "edit
text", which is almost never what you want. If the page already makes the
purpose plain, use aria-label. Do not leave the control unnamed.aria-label works on a Helix field because the component forwards it. Most
custom elements do not. A custom element cannot delegate a label to the control
inside its shadow root. An aria-label you write on the host stays on a wrapper
and never reaches the <input> that takes focus. We measured this in Helix
before we fixed it: the inner input had no label and no name at all.The field renders the label and clips it. The name then reaches the input
through the same <label for> that a visible label uses.hide-label was a boolean beside label, and we removed it. Setting a label
and then a second attribute to hide it is the same as having no visible label.Shared behavior is its own familyOption, Listbox, and Select are three families so that the keyboard
handling, the filtering, the multi-select rules, and the active-item tracking
are written once. Select renders a Listbox internally, and so does Combobox. A command
palette would too.For the same reason, we wrote DrilldownController. Since it was first written,
Menu had held a page stack and derived its rows, and none of that code
was specific to menus. Menu and Drilldown now share it.If you are about to write a second copy of some behavior, make it shared.