HelixOverviewComponentsMapsGraphIntroductionInstallationDeriving a themeSandboxTheme builderVisual testingCortexDocsEngramBuildingTraceBuildingRecallDeclaredServicesInsightsGet in touchSource on GitHub
Getting started
ReactThe elements are custom elements, and React renders them directly, so there is
no React package. For React, we ship a jsx.d.ts file that types every tag, and
a runtime bridge in @fusion.dev/helix-core that repairs two React behaviors.import'@fusion.dev/helix/jsx'Add this types-only import once, anywhere TypeScript sees it. <helix-button>
then has typed props, and a value the component does not accept is a compile
error.Bind events with onhelix-*<helix-accordion-itemonhelix-before-open={(event)=>track(event.detail.open)}/>The prop is on followed by the exact event name. onHelixOpen and onOpen do
not work. React 19 passes an unknown on* prop to addEventListener with the
name as written, so use the event's own casing.In a development build, an element prints a warning once when one of its on
props can never run:a misspelled event name, such as onhelix-opnean event that a different element sendsSolid's on: syntaxa callback property that was removed, such as onCheckedChangeTypeScript does not check a JSX prop whose name has a hyphen, so the console is
the only place these show. Vite and webpack use the development export
condition while you develop. A production build carries none of the check.The hydration bridgeWhile React hydrates, it skips everything it would have to apply imperatively.
That affects two things, and HelixElement repairs both.Event handlersReact stores the event handlers it sees during hydration on the DOM node, but it
does not bind them. An event fired before the next render reaches no listener.
The element reads the handlers React stored on the node and binds them, so a
handler works from the first paint.Object and function propertiesReact drops object and function properties on both sides. The server renderer
writes nothing for a prop whose value is an object, and hydration never applies
one. An array prop therefore never reaches the element, and because the server
and the client agree, React shows no warning. The element assigns these
properties itself once it upgrades, so a data table can take its rows as a plain
prop:<helix-data-tablelabel="People"rows={rows}><helix-table-columnfield="name"sortable>Name</helix-table-column><helix-table-columnfield="team"sortable>Team</helix-table-column><helix-table-columnfield="location">Location</helix-table-column></helix-data-table>The element assigns only properties the component declares, and only values an
attribute cannot carry. Strings still go through attributes, which already work.
The element also leaves a null alone, so the component keeps its default.The columns are elements for the same reason. field, sortable, and width
are strings and booleans, so they pass through server rendering intact. cell
and compare are functions, set on an element that repairs its own props.
Inside a columns array, the bridge could not reach them.The table cannot draw its cells on the server, because reading a cell needs the
columns and the server render does not pass the table its markup children. Write
the first page as rows. In the same server component, map the first page of the
array to helix-table-row and helix-table-cell elements under the columns.
Sort and page it as the table does, and format each value as its column draws it:constfirstPage=[...rows].sort((a,b)=>a.name.localeCompare(b.name)).slice(0,4)<helix-data-table default-sort="name:asc"label="People"page-size={4}rows={rows}><helix-table-columnfield="name"sortable>Name</helix-table-column><helix-table-column field="team"sortable>Team</helix-table-column>{firstPage.map((person)=>(<helix-table-rowkey={person.name}><helix-table-cellheader>{person.name}</helix-table-cell><helix-table-cell>{person.team}</helix-table-cell></helix-table-row>))}
</helix-data-table>The values are in the served HTML, and @fusion.dev/helix-styles/helix.css lays
them out as the table before the script runs. The table shows your rows until
rows holds a row, then draws its own in their place. It leaves your rows in the
page, so React finds the markup it rendered. A development build warns when the
first page the table draws shows different words from the rows you served.Size the columns with columns on the table, such as
columns="2fr 1fr max-content". The stylesheet can read the table's own
attribute and not a column's width, so the served rows are laid out at the
widths the table draws, and nothing moves when it upgrades. The checkbox column of a
selectable table and the pagination's row are held from the first paint too.The React versionThe bridge is runtime code in @fusion.dev/helix-core, which you already have,
and it needs no configuration. It reads a React internal: the per-page key that
React stores props under. If a React release renames that key, the bridge stops
repairing, and your handlers again need a commit before they fire. We pin the
React version we measured the bridge against, and our build fails when React
moves past it, so a rename cannot go unnoticed.Seed, mirror, or refuse a valueHelix differs from React in one way that surprises people. This markup
compiles, and the field stays editable:// Compiles, and the field stays editable.<helix-inputlabel="Name"value="Ada"/>In React, value on a native <input> without onChange renders a read-only
field and logs a warning. On a Helix element, value is a seed. The element
starts with that value and keeps applying its own changes, so the markup below
works without a handler.import{flushSync}from'react-dom'// Seeded: the element owns it from there.<helix-inputdefault-value="Ada"label="Name"/>// Mirrored: bind the notification and let the element apply.<helix-input label="Name"onChange={(event)=>setName(event.currentTarget.value)}/>// Refused: you take the decision for this one change.<helix-inputlabel="Name"onhelix-before-change={(event)=>{event.preventDefault()flushSync(()=>setName(clean(event.detail.value)))}}value={name}/>To refuse a change, call preventDefault(). Skipping setState does not
refuse it. React restores a native <input> whose state did not change, and it
does not do this for a custom element. A handler that does not call setState
leaves the element with the new value.Set the state inside flushSync when you write your own value. React sets
the priority of a state update from the type of the event being handled.
helix-before-change is not one of the types React lists, so the update gets
the default priority and renders in a later task. A key that arrives before that
task lands in the text from before the update, and the keys typed in between are
lost. A barcode scanner, a password manager, and a test send keys that fast:
Playwright typing Ada L! with no delay into a field that keeps lowercase
letters left it empty. flushSync renders the update before your handler
returns, so the element holds your value when the next key arrives. The same
applies to every field you type into, such as helix-textarea,
helix-number-input, and helix-pin-input.Until August 26, 2026, this page described the opposite behavior, and the typings
enforced it. Writing value made you the owner of the state. The element
applied no change until you set the value, and <helix-input value="Ada" />
without a handler was a compile error. We removed that mode and its
constraint.See State and control for the full model.Register in a client component'use client'// One line per family you use.import'@fusion.dev/helix/button'import'@fusion.dev/helix/dialog'exportconstHelixElements=()=>nullRender it once in your layout. Your pages stay server components. An
unregistered tag is an unknown element in the HTML, and it upgrades when the
definition loads. Your server components need no changes.Server components get no declarative shadow DOMReact inserts a server component's output by replaying the flight payload
through DOM APIs. The browser does not parse that output as HTML, and
declarative shadow DOM cannot activate for nodes inserted by script.
elements.css exists for this case. It styles the markup before the elements
upgrade, so the page looks finished before hydration.Server rendering covers server rendering in full, and
Using the elements covers the other binding rules.