Upgrading to 3.0.0
apps/docs/src/content/docs/migration/upgrading-to-3 Click to copy apps/docs/src/content/docs/migration/upgrading-to-3 HELiX 3.0.0 is the first major release to the enterprise healthcare channel. This guide walks through every breaking change between @helixui/library@2.1.2 and @helixui/library@3.0.0.
Scope and effort
Section titled “Scope and effort”This guide covers @helixui/library, @helixui/drupal-starter, and @helixui/drupal-behaviors. The @helixui/tokens and @helixui/react packages bump to 3.0.0 in lockstep; their deltas are summarized at the end.
Most 3.0.0 changes are renames with precise grep-able patterns. Expected effort for a production app:
- Small app (~50 HELiX usages): 1–2 hours.
- Mid-sized app (~200 usages): half a day.
- Large app (~1000+ usages, multiple packages): 1 day with scripted find-and-replace.
- Mid-sized Drupal site (~150 SDC usages, forked templates): additional half day — see Drupal integration.
Codemod support is tracked for 3.0.1. For 3.0.0, each section below provides a precise regex for scripted replacement with sd or ripgrep-replace.
Pre-upgrade checklist
Section titled “Pre-upgrade checklist”- Pin current versions in a branch (
git checkout -b chore/helix-3-upgrade). - Run your test suite on 2.1.2 to capture a green baseline.
- Confirm your integration does not import from undocumented deep paths. Grep for
@helixui/library/src/— anything found will break. - Confirm your bundler preserves the
sideEffectsfield — HELiX requires the first-import side effect to adopt design tokens at the document level.
1. Install
Section titled “1. Install”pnpm add @helixui/library@^3.0.0 @helixui/tokens@^3.0.0React consumers:
pnpm add @helixui/react@^3.0.0CDN consumers: pin https://unpkg.com/@helixui/library@3.0.0/dist/cdn/helix-core-3.0.0.min.js plus the per-component modules you use.
2. aria-label → accessible-label
Section titled “2. aria-label → accessible-label”Every component that previously accepted aria-label or hxAriaLabel now publishes the public attribute as accessible-label (property: accessibleLabel).
Why. ARIA 1.2 reserves aria-* attributes as host-scoped ARIA state, not authored component API. accessible-label makes the component’s public label surface explicit.
<!-- Before --><hx-button aria-label="Close dialog">×</hx-button>
<!-- After --><hx-button accessible-label="Close dialog">×</hx-button>// Beforeel.hxAriaLabel = 'Close dialog';
// Afterel.accessibleLabel = 'Close dialog';Codemod:
rg -P -l '<hx-(?!card\b)[a-z-]+[^>]*aria-label=' | xargs sd '(<hx-(?!card\b)[a-z-]+[^>]*?)aria-label=' '$1accessible-label='3. ::part(error-message) → ::part(error)
Section titled “3. ::part(error-message) → ::part(error)”Form controls rename the validation-message part from error-message to error, aligning with help-text.
/* Before */hx-text-input::part(error-message) { color: var(--my-error);}
/* After */hx-text-input::part(error) { color: var(--my-error);}Codemod:
rg -l '::part\(error-message\)' | xargs sd '::part\(error-message\)' '::part(error)'4. hx-date-picker / hx-time-picker popup is still a focus-trapping dialog
Section titled “4. hx-date-picker / hx-time-picker popup is still a focus-trapping dialog”The calendar / clock popup continues to render a native <dialog> element following the W3C APG
date-picker dialog pattern, and continues to trap focus inside the popup while open. If your
upgrade notes from an older draft of this guide promised a non-modal popup or a focus-trap
removal, those changes did not ship — Escape still closes and restores focus, click-outside
still closes, arrow-key navigation is unchanged, and Tab cycles inside the popup until the
user dismisses it.
If you authored CSS targeting ::backdrop against the dialog, those rules still apply. There is
nothing to remove for this version. Tests asserting focus-trap behavior on hx-date-picker /
hx-time-picker continue to pass.
5. hx-dialog — modal defaults to false
Section titled “5. hx-dialog — modal defaults to false”The modal property now defaults to false, matching HTML boolean-attribute semantics. To preserve 2.x behavior, add modal explicitly:
<!-- Before: modal by default --><hx-dialog open>...</hx-dialog>
<!-- After: explicit opt-in to top-layer + backdrop + focus trap --><hx-dialog open modal>...</hx-dialog>Find missing attributes:
rg '<hx-dialog\b(?![^>]*\bmodal\b)' --type html --type tsx --type vuerg '<hx-dialog\b(?![^>]*\bmodal\b)' -g '*.twig' -g '*.html.twig'Audit each hit. Confirmation dialogs, clinical alerts, and blocking workflows should add modal. Inline drawers and sidebar panels should stay non-modal.
6. hx-phi-field — PHI set via JS property, never as an HTML attribute
Section titled “6. hx-phi-field — PHI set via JS property, never as an HTML attribute”hx-phi-field holds Protected Health Information via the data JS property. The property is declared with @property({ attribute: false }) so Lit does not observe a data HTML attribute. If raw HTML markup sets <hx-phi-field data="...">, the component’s connectedCallback reassigns the value onto the data property and removes the attribute from the live DOM so the PHI is not retained in outerHTML or DevTools after upgrade. In development builds the rescue also emits a console.warn; production builds strip the warn at build time.
This runtime rescue is a mitigation, not a cleanup. Any value shipped as an HTML attribute is already present in the SSR / server-rendered source, the HTTP response body, View Source, browser caches, and any access logs that recorded the response, all of which the client cannot reach. Never place PHI in HTML markup — always assign it to the data JS property on a live element reference.
The supported pattern — set data via JS:
<hx-phi-field id="mrn" field-id="patient-mrn" field-type="mrn"></hx-phi-field>const el = document.querySelector('hx-phi-field#mrn');el.data = '123-45-6789';The unsupported pattern — setting via HTML attribute:
<!-- Do not do this. The client-side rescue removes the attribute from the live DOM after upgrade, but the raw value has already been sent to the browser in the SSR source and remains in view-source, HTTP response bodies, access logs, and caches. Never ship PHI in HTML markup. --><hx-phi-field data="123-45-6789"></hx-phi-field>Consumers setting PHI via the data property need no change. Consumers setting PHI via an HTML attribute must move the assignment to JavaScript.
This change surfaced during the Figgy (Northwell) integration audit as a HIPAA-relevant exposure vector.
7. FormMixin consolidation
Section titled “7. FormMixin consolidation”All 15 form-associated components now compose FormMixin(HelixElement) and inherit shared interaction-state tracking.
Inherited for free:
dirty— true after first value mutationtouched— true after first blurpristine— opposite ofdirtycheckValidity()/reportValidity()— delegate toElementInternals_updateValidity()— override hook called after everyupdated()cycle
Subclassing example:
import { HelixElement, FormMixin } from '@helixui/library';
class MyCustomInput extends FormMixin(HelixElement) { static override formAssociated = true;
override _updateValidity(): void { if (this.required && !this.value) { this._internals.setValidity({ valueMissing: true }, 'Required', this._inputEl); } else { this._internals.setValidity({}); } }
override _onFormReset(): void { this.value = ''; this._resetInteractionState(); this._internals.setFormValue(''); }}If you previously implemented dirty / touched locally, delete it and rely on the inherited getters. Wire your native input’s input event to this._handleInteractionInput() and the blur event to this._handleInteractionBlur().
8. Subclassing contract — @protected override hooks
Section titled “8. Subclassing contract — @protected override hooks”HelixElement and FormMixin hooks are now part of the public subclassing contract (previously @internal). Breaking changes to their signatures are gated to major releases.
| Base class / mixin | Hook | When called |
|---|---|---|
HelixElement | _onFormDisabled(disabled) | Parent <fieldset> disabled toggled |
HelixElement | _onFormReset() | Owning form reset |
HelixElement | _onFormStateRestore(state, mode) | Form state restore (bfcache / autofill) |
FormMixin | _handleInteractionInput() | Call from native input event |
FormMixin | _handleInteractionBlur() | Call from native blur event |
FormMixin | _resetInteractionState() | Call from _onFormReset() |
FormMixin | _updateValidity() | Override for constraint logic |
Consumers already using these hooks in 2.1.x need no code change — the protected access modifier was already in place. 3.0.0 promotes them from internal to stable contract.
9. Deprecated symbols removed
Section titled “9. Deprecated symbols removed”Wc* type aliases
Section titled “Wc* type aliases”// Beforeimport type { WcButton, WcCard, WcSwitch } from '@helixui/library';
// After — class types use Helix* naming; CustomEvent detail types// and a small set of legacy re-exports retain Hx* naming.import type { HelixButton, HelixCard, HxSwitch } from '@helixui/library';
// Event-detail types use Hx* naming:import type { HxButtonClickDetail, HxSwitchChangeDetail } from '@helixui/library';Codemod:
rg -l "from '@helixui/library'" | xargs sd "\bWc([A-Z][a-zA-Z]+)\b" 'Hx$1'2.0.0 property-rename shims
Section titled “2.0.0 property-rename shims”The back-compat shims preserving pre-2.0 property names are removed.
| Component | Removed (deprecated in 2.0) | Use |
|---|---|---|
hx-card | hxHref | href |
hx-card | hxAriaLabel | label |
hx-field | hxSize | size |
hx-banner, hx-dialog, hx-drawer, hx-toast | closeLabel | labelClose |
hx-split-button | triggerLabel | labelTrigger |
hx-split-button, hx-overflow-menu | menuLabel | labelMenu |
mergeTokenStyles utility
Section titled “mergeTokenStyles utility”// Beforeimport { mergeTokenStyles } from '@helixui/library/utilities';static styles = mergeTokenStyles(ownStyles);
// After — tokens adopt at document level automaticallystatic styles = ownStyles;10. @floating-ui/dom dynamic import
Section titled “10. @floating-ui/dom dynamic import”hx-popover, hx-dropdown, hx-overflow-menu, hx-popup, and hx-tooltip load
@floating-ui/dom dynamically on first interaction. hx-select and hx-combobox do not — they
position their listbox via custom CSS without the floating-ui dependency.
What you’ll see:
- A separate
chunk-floating-ui-*.jsin your build output. - First interaction fetches the chunk (~12KB min+gz).
- Subsequent interactions use the cached chunk.
Strict CSP: ensure script-src allows the chunk origin. For same-origin bundles this is a no-op.
Pre-warming: import the positioning utility at app startup.
import '@floating-ui/dom';11. Public-API allowlist
Section titled “11. Public-API allowlist”3.0.0 blocks undocumented JavaScript/TypeScript deep imports. For JS/TS symbols, the public surface is the root barrel (@helixui/library) and per-component entry points (@helixui/library/components/<hx-name>). There is no @helixui/library/mixins subpath export — FocusMixin, FormMixin, HelixElement, and HelixAuditController are all re-exported from the root barrel. Consumers importing other JS/TS subpaths will see a build-time error.
Documented non-JS asset subpaths (public CSS bundles under @helixui/library/dist/css/*.css, @helixui/library/fouc.css, and @helixui/library/custom-elements.json) remain part of the public surface via their package export paths and are unaffected by this rule.
// Before — blocked in 3.0.0import { _internalHelper } from '@helixui/library/src/internal/helper.js';
// After — open an issue to request public export, or copy the helper locally.If you relied on an undocumented deep import, open an issue — we can promote the symbol in a minor release.
12. CDN delivery
Section titled “12. CDN delivery”3.0.0 makes Strategy B the recommended path.
Strategy B (recommended) — core + per-component
Section titled “Strategy B (recommended) — core + per-component”<script type="module" src="https://unpkg.com/@helixui/library@3.0.0/dist/cdn/helix-core-3.0.0.min.js"></script>
<script type="module" src="https://unpkg.com/@helixui/library@3.0.0/dist/cdn/components/hx-button-3.0.0.js"></script><script type="module" src="https://unpkg.com/@helixui/library@3.0.0/dist/cdn/components/hx-card-3.0.0.js"></script>Core is ~8.4KB min+gz. Each component is ~2KB.
Strategy A (kitchen sink — back-compat only)
Section titled “Strategy A (kitchen sink — back-compat only)”<script type="module" src="https://unpkg.com/@helixui/library@3.0.0/dist/cdn/helix-3.0.0.min.js"></script>Strategy A may be removed in a future major.
13. Adopted stylesheets side effect
Section titled “13. Adopted stylesheets side effect”HELiX adopts design tokens at the document level via document.adoptedStyleSheets on first import of any component. This is the only supported theming path.
package.jsonsideEffectsmust remain truthy (or include the token-adoption module).- Bundlers with aggressive dead-code elimination must preserve the first-import side effect.
- CDN consumers get adoption automatically.
If your build strips side effects, HELiX components render without tokens (no colors, no spacing, no typography). Fix at the bundler config level.
14. Tokens package
Section titled “14. Tokens package”@helixui/tokens@3.0.0:
tokenStylesexport (deprecated in 2.1.2) removed. UselightTokenCssfor raw CSS or rely on automatic document-level adoption.- Semantic-tier token alignment — see the CHANGELOG for the complete delta.
14.1 Border-on-dark token rename (3.2.2)
Section titled “14.1 Border-on-dark token rename (3.2.2)”In @helixui/tokens@3.2.2, two tokens that shipped in 3.2.0 / 3.2.1 were renamed because their values are translucent fills (30% / 10% white alpha) — they cannot satisfy the WCAG 1.4.11 3:1 contrast floor required of borders, and were always functioning as fills:
| 3.2.0 / 3.2.1 (deprecated) | 3.2.2 (canonical) |
|---|---|
--hx-color-border-on-dark-default | --hx-color-surface-on-dark-overlay-default |
--hx-color-border-on-dark-subtle | --hx-color-surface-on-dark-overlay-subtle |
--hx-color-border-on-dark-strong is unchanged — its 70% alpha satisfies the 3:1 border contract and remains under border.*.
Component-internal consumption is safe in either direction. Every hx-* rule that paints with these tokens reads the deprecated and canonical names with a both-name fallback chain, so a consumer override on either name reaches paint:
hx-button[variant='tertiary'][inverted] { /* Either of these works in 3.2.2: */ --hx-color-border-on-dark-subtle: rgb(7, 8, 9); --hx-color-surface-on-dark-overlay-subtle: rgb(10, 11, 12);}Direct readers must migrate. The deprecated names are no longer emitted at :root in dist/tokens.css. Downstream code that reads them outside of an hx-* component will resolve to an empty value:
/* Downstream Drupal/app stylesheet (NOT inside an hx-* component shadow root) */.my-custom-overlay { /* 3.2.0 / 3.2.1: resolves to var(--hx-overlay-white-30) */ /* 3.2.2: resolves to empty */- background-color: var(--hx-color-border-on-dark-default);+ background-color: var(--hx-color-surface-on-dark-overlay-default);}// 3.2.0 / 3.2.1: returns "rgba(255, 255, 255, 0.3)" (or similar)// 3.2.2: returns ""getComputedStyle(document.documentElement).getPropertyValue('--hx-color-border-on-dark-default');getComputedStyle(document.documentElement).getPropertyValue('--hx-color-surface-on-dark-overlay-default');Why we did not emit a :root alias. Two alias variants were considered. Both fail at the same consume-site read order — and both would silently break the documented host-scoped canonical-override path.
Variant A (var() alias) — :root { --hx-color-border-on-dark-default: var(--hx-color-surface-on-dark-overlay-default); }. The inner var() resolves at :root’s computed-value time (CSS Custom Properties §3) and inherits down to every descendant as an opaque resolved value. Host-scoped consumer overrides on the canonical name are silently shadowed at every component consume site, because the var() chain reads the deprecated name first and finds it set (via inheritance) rather than falling through.
Variant B (concrete-value alias) — :root { --hx-color-border-on-dark-default: rgba(255, 255, 255, 0.30); } plus a paired .dark rule. No inner var() to substitute, but inheritance still delivers a non-empty value to every descendant. Same failure: the consume site reads the deprecated name first and never falls through. Variant B has additional cost: a literal value at :root breaks the primitive chain (a consumer who overrides --hx-overlay-white-30 would see that change reflected in the canonical token but NOT in the deprecated alias).
Both variants would fail the canonical-override regression test (dark-mode-resolution.test.ts:219-227). We chose to break the undocumented direct-reader path explicitly rather than break the documented host-override path silently. The deprecated names are scheduled for hard removal in 4.0.0.
15. React wrapper
Section titled “15. React wrapper”@helixui/react@3.0.0:
ariaLabel→accessibleLabeleverywhere.Wc*type imports removed; useHx*.- Event details exported as named types (
HxClickEvent,HxChangeEvent, etc.) instead of anonymousCustomEvent<unknown>.
16. Drupal integration (@helixui/drupal-starter and @helixui/drupal-behaviors)
Section titled “16. Drupal integration (@helixui/drupal-starter and @helixui/drupal-behaviors)”Both Drupal packages bump to 3.0.0 in lockstep with the library. The changes are a direct consequence of the library renames in §2 through §6 — SDC templates and Drupal behaviors are realigned with the canonical component API.
16.1 @helixui/drupal-starter — SDC template changes
Section titled “16.1 @helixui/drupal-starter — SDC template changes”Shipped SDC templates (packages/drupal-starter/components/<component>/<component>.twig) are realigned with the 3.0.0 library public API. Consumers that installed the module unmodified pick up the new templates on upgrade. Consumers with forked templates must re-apply their customizations against the new 3.0.0 base.
hx-card — accessible-label → hx-label
Section titled “hx-card — accessible-label → hx-label”hx-card’s interactive-link label uses the hx-label HTML attribute (JS property stays label), not accessible-label. Same exception as §2.
{# BEFORE (2.1.2) #}<hx-card variant="{{ variant|default('default') }}" {% if href %}hx-href="{{ href }}" accessible-label="{{ aria_label }}"{% endif %}> {{ content }}</hx-card>
{# AFTER (3.0.0) #}<hx-card variant="{{ variant|default('default') }}" {% if href %}hx-href="{{ href }}" hx-label="{{ aria_label }}"{% endif %}> {{ content }}</hx-card>All ARIA-labelable components — aria-label → accessible-label
Section titled “All ARIA-labelable components — aria-label → accessible-label”Every template that previously emitted aria-label on a HELiX component tag (hx-button, hx-text-input, hx-form, hx-steps, etc.) now emits accessible-label. This matches the library-side rename in §2.
{# BEFORE #}<hx-button variant="{{ variant }}" {% if aria_label %}aria-label="{{ aria_label }}"{% endif %}>{{ label }}</hx-button>
{# AFTER #}<hx-button variant="{{ variant }}" {% if aria_label %}accessible-label="{{ aria_label }}"{% endif %}>{{ label }}</hx-button>hx-dialog — modal default flipped to false
Section titled “hx-dialog — modal default flipped to false”The library changed hx-dialog’s modal default from true to false (see §5). The starter’s hx-dialog.twig now adds modal explicitly where modal semantics are required.
{# BEFORE — implicit modal #}<hx-dialog {% if open %}open{% endif %} heading="{{ heading }}"> {{ content }}</hx-dialog>
{# AFTER — modal is opt-in #}<hx-dialog {% if open %}open{% endif %} {% if modal ?? true %}modal{% endif %} heading="{{ heading }}"> {{ content }}</hx-dialog>Audit each call site of the dialog SDC. Confirmation dialogs, clinical alerts, and blocking workflows should pass modal: true. Inline drawers, sidebar panels, and toast-adjacent UIs should pass modal: false to preserve 3.0.0 non-modal behavior.
hx-date-picker / hx-time-picker — non-modal popup contract
Section titled “hx-date-picker / hx-time-picker — non-modal popup contract”The library migrated both pickers from a native modal <dialog> to a non-modal popup (see §4). The starter templates are unchanged at the markup level — the picker elements expose the same attribute surface — but consumers with CSS overrides must drop any ::backdrop rules and audit picker stacking.
- Remove
::backdropoverrides from theme CSS targetinghx-date-picker/hx-time-picker. - Tests in the consumer theme that asserted focus-trap behavior on the picker popup must be updated. Escape-close and focus-restoration behavior is unchanged.
- If your site wraps the picker in a custom stacking context (drawer, modal-within-modal), you may need to raise the popup’s
z-index.
Cross-reference §4 for the full behavioral delta.
::part(error-message) → ::part(error)
Section titled “::part(error-message) → ::part(error)”Theme CSS snippets that style form-validation messages use ::part(error).
/* BEFORE */hx-text-input::part(error-message) { color: var(--hx-color-danger);}
/* AFTER */hx-text-input::part(error) { color: var(--hx-color-danger);}Audit any theme-level CSS overrides in your Drupal theme (*.css, *.scss under themes/custom/*) for the old selector name.
hx-phi-field — no data attribute in SSR HTML
Section titled “hx-phi-field — no data attribute in SSR HTML”The library strips a data attribute off hx-phi-field in connectedCallback to prevent PHI retention in outerHTML / DevTools after hydration (see §6). The starter’s hx-phi-field.twig does not render data as an HTML attribute — PHI is passed to the component via the data JS property (@property({ attribute: false }) — Lit does not observe it), set client-side through a Drupal behavior.
The runtime rescue only removes the data attribute from the live DOM after hydration; it does not touch any other attribute, and it cannot remove any attribute from the SSR HTML, the HTTP response body, View Source, browser caches, or access logs that recorded the response. Any server-rendered PHI — including a hand-authored value attribute or any other custom attribute on hx-phi-field — must be removed at template-render time. The supported pattern for Drupal:
{# Server-rendered SDC — no PHI in the attribute surface #}<hx-phi-field id="mrn-{{ patient_id }}" field-type="mrn" field-id="patient-mrn"></hx-phi-field>// Drupal.behaviors — assign PHI on the live element after attach.Drupal.behaviors.hxPhiPopulate = { attach(context) { once('hx-phi-populate', 'hx-phi-field[id^="mrn-"]', context).forEach((el) => { // PHI source must be role-gated server-side via drupalSettings. const phi = drupalSettings.helix?.phiByElementId?.[el.id]; if (phi) { el.data = phi; } }); },};PHI is assigned via JavaScript (drupalSettings) instead of element attributes, so it is not baked into the SSR HTML response body, View Source, browser caches, or access logs that recorded the response. drupalSettings is still client-visible response data — it ships as an inline <script type="application/json"> payload readable by any script in the page — so keep PHI payloads minimal, enforce role-based access server-side before emitting the payload, and cover consumption with existing audit controls, including hx-phi-access handling.
16.2 @helixui/drupal-behaviors — API realignment
Section titled “16.2 @helixui/drupal-behaviors — API realignment”@helixui/drupal-behaviors ships Drupal.behaviors wrappers around the interactive HELiX components (hx-accordion, hx-dialog, hx-drawer, hx-menu, hx-popover, hx-tabs, hx-toast, hx-tooltip). The 3.0.0 bump realigns them with the canonical library surface.
FormMixin event-surface migration
Section titled “FormMixin event-surface migration”Behaviors that listen for form events on the 15 form-associated components are rewired to the consolidated FormMixin event surface. If a custom behavior subscribed to per-component events (for example hx-text-input-input, hx-select-change), migrate to the mixin-level events documented in §7.
See §7 for the inherited interaction-state model (dirty, touched, pristine, _handleInteractionInput, _handleInteractionBlur). Custom Drupal behaviors that previously implemented these locally should delete the local implementation and read the inherited getters.
accessible-label writes
Section titled “accessible-label writes”Behaviors that dynamically set or update a component’s accessible name now write accessible-label where they previously wrote aria-label — matching the library rename in §2. This applies to any behavior that calls element.setAttribute('aria-label', ...) or element.setAttribute('hxAriaLabel', ...) on a HELiX component. Native HTML element writes (<button>, <input>) continue to use aria-label.
Component tag name corrections
Section titled “Component tag name corrections”Internal tag references in the behaviors are corrected to match the shipped element names. No consumer API change — if your code uses the exported behavior factories directly, recompile against 3.0.0 types to pick up the corrections.
TypeScript consumers — dist/ type declarations
Section titled “TypeScript consumers — dist/ type declarations”@helixui/drupal-behaviors@3.0.0 ships dist/index.d.ts and per-behavior type declarations. TypeScript consumers no longer need to augment Drupal.behaviors manually:
// TypeScript consumer — 3.0.0import '@helixui/drupal-behaviors';// Types for Drupal.behaviors.hxDialog, hxDrawer, etc. are now resolved.Peer range — backward-compatible with 2.x
Section titled “Peer range — backward-compatible with 2.x”{ "peerDependencies": { "@helixui/library": "^2.1.2 || ^3.0.0" }}The peer range stays wide on purpose. Consumers can install @helixui/drupal-behaviors@3.0.0 alongside @helixui/library@2.1.2 during a staged migration without the package manager complaining. The actual library upgrade still requires the attribute and CSS-part renames in this guide.
16.3 Reconciliation checklist — Drupal consumers with forked templates
Section titled “16.3 Reconciliation checklist — Drupal consumers with forked templates”Run these greps against your Drupal theme and any custom modules that fork the starter templates. Each hit is a 3.0.0 migration task.
# 1. hx-card accessible-label → hx-label (scope: hx-card templates only)rg -n 'accessible-label' --type twig -g '*hx-card*' -g '*card*'
# 2. Invalid hx-size long-form values on size-aware components# The hx-icon, hx-badge, and hx-button size enums are short-form only:# - hx-icon: xs | sm | md | lg | xl# - hx-badge: sm | md | lg# - hx-button: sm | md | lg# Long-form "small" / "medium" / "large" never matched any of these enums;# any hit is a defect. Convert to the correct short-form alias per# component.# Note: `--type twig` matches both *.twig and *.html.twig — no extra glob# is required for Drupal's hyphenated template extension.rg -n 'hx-size="(small|medium|large)"' --type twig
# 3. ::part(error-message) in theme CSSrg -n '::part\(error-message\)' -g '*.css' -g '*.scss'
# 4. Native <dialog> inside picker templates (library no longer uses it)rg -n '<dialog\b' --type twig -g '*date-picker*' -g '*time-picker*'
# 5. aria-label on hx-* component tags (requires PCRE2 ripgrep)rg -P -n '<hx-(?!card\b)[a-z-]+[^>]*\baria-label=' --type twig
# 6. hx-phi-field rendering value or data as an HTML attributerg -n '<hx-phi-field[^>]*\b(value|data)=' --type twigAfter each fix, run your Drupal theme’s regression suite (or the site’s visual regression harness) against the affected templates. The renames are mechanical; regressions usually surface as missing accessible names in axe, failing focus tests on the picker popup, or a flat/non-modal dialog where a modal was expected.
A grep-based codemod that automates the first five patterns is tracked for 3.0.1 — same follow-up as the library-side codemod.
Rollback
Section titled “Rollback”If 3.0.0 surfaces a critical defect:
pnpm add @helixui/library@2.1.2 @helixui/tokens@2.1.2Open an issue with a reproduction. HELiX maintains the 2.1.x line with security-only patches for six months post-3.0.0.
Getting help
Section titled “Getting help”- Migration questions: open an issue tagged
migration-3.0. - CEM / API reference: Starlight auto-docs at
/api. - Subclassing contract:
packages/hx-library/src/base/helix-element.tsandpackages/hx-library/src/mixins/FormMixin.ts.