Skip to content
HELiX

Extending HELiX Components

apps/docs/src/content/docs/extending/overview Click to copy
Copied! apps/docs/src/content/docs/extending/overview

HELiX components are built to be extended, not just used. This page orients enterprise teams on the architectural posture of the library and the four vectors available for customization — before writing a single line of code.

Every HELiX component extends LitElement directly. There is no intermediate base class, no HxBaseElement, no mixin tower between your code and the platform.

LitElement (Lit 3.x)
├── HxButton
├── HxCard
├── HxTextInput
├── HxDialog
└── ... (81 components total)

Why this matters for extension:

  • You can subclass any HELiX component and get a clean super chain — no hidden lifecycle layers to debug
  • TypeScript types are precise and complete — no inherited overloads from an abstraction layer
  • Lit’s own extension patterns (mixins, controllers) work without friction
  • Upgrading HELiX does not silently change behavior through a shared base class

The library is deliberately thin on convention. Convention lives in the token system and the Custom Elements Manifest — not in the class hierarchy.

┌─────────────────────────────────────────────────────────────────┐
│ HELiX Extension Surface │
├───────────────────┬─────────────────────────────────────────────┤
│ CSS Custom Props │ Override visual design via token cascade │
│ (--hx-*) │ Works in any stylesheet, Twig, or inline │
├───────────────────┼─────────────────────────────────────────────┤
│ CSS ::part() │ Style specific internal elements directly │
│ Selectors │ Bypasses shadow DOM encapsulation cleanly │
├───────────────────┼─────────────────────────────────────────────┤
│ Slot Composition │ Project content into defined slots │
│ │ Wrap components without subclassing │
├───────────────────┼─────────────────────────────────────────────┤
│ Class Inheritance│ Subclass to add properties, methods, │
│ (extends HxFoo) │ override render, or register a new tag │
└───────────────────┴─────────────────────────────────────────────┘

Vector 1: CSS Custom Properties (Token Override)

Section titled “Vector 1: CSS Custom Properties (Token Override)”

HELiX components expose --hx-* CSS custom properties at two levels:

  • Semantic tokens (--hx-color-primary-500, --hx-space-4) — affect the entire library at once
  • Component tokens (--hx-button-bg, --hx-card-padding) — scope to a single component

Override at the root for global theming:

:root {
--hx-color-primary-500: #005eb8; /* NHS Blue — affects all components */
--hx-color-error-500: #d0021b; /* Clinical alert red */
--hx-border-radius-md: 2px; /* Tighter corners for clinical UI */
}

Override at the component level for targeted customization:

hx-button[variant="danger"] {
--hx-button-bg: var(--hx-color-error-500);
--hx-button-color: #ffffff;
}

Override inline for one-off instances:

<hx-alert style="--hx-alert-border-color: #ff6b35;">
Medication interaction detected
</hx-alert>

Reach: Any stylesheet — global CSS, Twig templates, React style prop, or Shadow DOM ::slotted() context. The cascade works everywhere.

Vector 2: CSS ::part() Selectors (Style Injection)

Section titled “Vector 2: CSS ::part() Selectors (Style Injection)”

Every HELiX component exposes named part attributes on its internal elements. The ::part() pseudo-element lets you style those internals from outside the shadow boundary — without modifying the component source.

/* Style the internal button element of hx-button */
hx-button::part(button) {
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
}
/* Style the label inside hx-text-input */
hx-text-input::part(label) {
font-size: 0.75rem;
color: var(--hx-color-text-secondary);
}
/* Target the track of hx-slider */
hx-slider::part(track) {
height: 6px;
border-radius: 3px;
}

Parts are always composable with other selectors:

/* Disabled state via attribute + part */
hx-button[disabled]::part(button) {
opacity: 0.4;
cursor: not-allowed;
}
/* Focus ring customization */
hx-text-input:focus-within::part(input) {
outline: 3px solid var(--hx-color-focus-ring);
outline-offset: 2px;
}

Every component’s available parts are listed in Storybook and in the Custom Elements Manifest.

Vector 3: Slot Composition (Content Projection)

Section titled “Vector 3: Slot Composition (Content Projection)”

Slots let you inject content into a component’s shadow DOM without touching its internals. This is the primary mechanism for building wrapper components and branded variants in Drupal.

Default slot — plain content projection:

<hx-card>
<p>Patient vitals recorded at 14:32. All within normal range.</p>
</hx-card>

Named slots — structured content composition:

<hx-card>
<span slot="header">Patient: Sarah Chen — MRN 00847291</span>
<div slot="body">
<hx-badge variant="success">Stable</hx-badge>
<p>Last updated 4 minutes ago</p>
</div>
<div slot="footer">
<hx-button size="sm">View Full Record</hx-button>
</div>
</hx-card>

Slots compose with other components for compound patterns:

<hx-dialog>
<span slot="title">Confirm Medication Order</span>
<hx-alert slot="body" variant="warning">
This dosage exceeds the recommended daily limit.
</hx-alert>
<div slot="footer">
<hx-button variant="secondary">Cancel</hx-button>
<hx-button variant="danger">Override and Submit</hx-button>
</div>
</hx-dialog>

In Twig, slots map directly to named attributes on the <slot> element:

<hx-card>
<span slot="header">{{ card.title }}</span>
{{ card.body }}
</hx-card>

Vector 4: Class Inheritance (Behavioral Extension)

Section titled “Vector 4: Class Inheritance (Behavioral Extension)”

When token overrides and slots are not enough — when you need new reactive properties, overridden lifecycle methods, or a registered custom element tag — subclass directly:

import { HxButton } from '@helixui/library';
import { property } from 'lit/decorators.js';
import { html } from 'lit';
export class ClinicalActionButton extends HxButton {
/** Requires two-click confirmation before firing hx-click */
@property({ type: Boolean }) requireConfirm = false;
private _pendingConfirm = false;
override handleClick(e: MouseEvent): void {
if (this.requireConfirm && !this._pendingConfirm) {
e.stopImmediatePropagation();
this._pendingConfirm = true;
this.requestUpdate();
setTimeout(() => {
this._pendingConfirm = false;
this.requestUpdate();
}, 3000);
return;
}
this._pendingConfirm = false;
super.handleClick(e);
}
override render() {
return this._pendingConfirm
? html`<button part="button">Click again to confirm</button>`
: super.render();
}
}
customElements.define('clinical-action-button', ClinicalActionButton);

Subclassing inherits all Lit reactive properties, event handling, and accessibility semantics. You extend behavior — you do not reimplement it.


Use this tree to choose the right vector before writing code:

Does the change affect only visual appearance?
├─ YES → Does it apply globally (all instances)?
│ │
│ ├─ YES → Override a semantic token at :root
│ │ (--hx-color-primary-500, --hx-space-4)
│ │
│ └─ NO → Does it apply to a specific component?
│ │
│ ├─ YES, surface-level → Override a component token
│ │ (--hx-button-bg, --hx-card-radius)
│ │
│ └─ YES, internal element → Use ::part() selector
│ (hx-button::part(button))
└─ NO → Does the change add or replace content inside the component?
├─ YES → Use slot composition
│ (slot="header", slot="footer", default slot)
└─ NO → Do you need new reactive properties, methods, or a new tag?
├─ YES → Subclass and extend
│ (class MyButton extends HxButton)
└─ NO → You may not need extension — check the component's
existing properties and slots first
GoalVectorStabilityExample
Brand color themingCSS custom property (primitive)Stable--hx-color-primary-500: #005eb8
Component-specific styleCSS custom property (component)Stable--hx-button-bg: navy
Style an internal DOM element::part() selectorStablehx-button::part(button)
Inject content into a componentSlotStable<span slot="header">
Add properties or new tagClass inheritanceStable (public API only)class X extends HxButton
Patch internal DOM structureUnstable — do not do thisShadow DOM internals are private

HELiX follows Semantic Versioning 2.0.0. The versioning guarantee applies per extension vector:

The Custom Elements Manifest (custom-elements.json) is the authoritative source of the public API. Anything listed there is guaranteed stable across patch and minor releases:

CEM FieldWhat It CoversVector
customElements[].tagNameThe hx-* tag nameAll vectors
customElements[].attributesHTML attributes and reflected propertiesTokens, inheritance
customElements[].cssProperties--hx-* CSS custom propertiesToken override
customElements[].cssPartsNamed CSS parts (::part())::part() selectors
customElements[].slotsNamed and default slotsSlot composition
customElements[].eventsCustom events (hx-*)Inheritance
customElements[].members (public)Public properties and methodsInheritance

Breaking changes to any of these require a major version bump and an advance deprecation window.

The following are implementation details. They may change in any release without a major version bump:

  • Internal shadow DOM structure (DOM nodes inside the shadow root)
  • Unexposed CSS class names inside shadow DOM
  • Private TypeScript properties and methods (prefixed with _ or marked @private)
  • Internal event handler names
  • Undocumented CSS custom properties not in the CEM

Do not build on shadow DOM internals. If you find yourself using shadowRoot.querySelector() or targeting unlisted CSS classes, you are building on unstable ground. File an issue requesting a named ::part() or a slot instead.

VectorStable GuaranteeBreaking Change Example
CSS custom propertiesAll --hx-* in CEMRemoving --hx-button-bg
::part() selectorsAll parts listed in CEMRenaming part="button" to part="control"
SlotsAll slots listed in CEMRemoving slot="header"
Public inheritanceAll public members in CEMRenaming handleClick() to onClick()
Internal DOMNoneAny internal restructuring

The generated manifest lives at packages/hx-library/custom-elements.json. You can also browse component APIs per-component in Storybook:

Terminal window
# View all CSS parts for hx-button
cat packages/hx-library/custom-elements.json | \
jq '.modules[].declarations[] | select(.tagName == "hx-button") | .cssParts'
# View all slots for hx-dialog
cat packages/hx-library/custom-elements.json | \
jq '.modules[].declarations[] | select(.tagName == "hx-dialog") | .slots'

Most enterprise customization falls into one of three strategies:

Use tokens and ::part() selectors. No new files, no build step, works directly from Twig or global CSS. The right choice for:

  • Brand theming (colors, typography, spacing)
  • Density adjustments (compact vs. comfortable layouts)
  • Clinical UI patterns (high contrast, larger touch targets)
  • Single-instance overrides in a specific page template
/* Tightly-scoped clinical override */
.patient-vitals-panel hx-badge::part(label) {
font-size: 1.125rem;
font-weight: 700;
}

Wrap HELiX components with slots in a Twig template, a Lit component, or a React component. No subclassing — just structural composition. The right choice for:

  • Application-specific compound components (PatientSummaryCard built from hx-card + hx-badge + hx-button)
  • Drupal paragraph types that combine multiple HELiX elements
  • Design system layers that add business logic around standard components
<!-- Drupal: patient-summary-card.html.twig -->
<hx-card class="patient-summary">
<span slot="header">
{{ patient.name }} — MRN {{ patient.mrn }}
</span>
<div slot="body">
<hx-badge variant="{{ patient.status_variant }}">
{{ patient.status_label }}
</hx-badge>
</div>
</hx-card>

Subclass a HELiX component when you need new behavior that cannot be composed from the outside. The right choice for:

  • Domain-specific interaction patterns (double-confirm buttons for destructive actions)
  • New reactive properties that change rendering
  • A registered custom element with your team’s tag prefix (clinical-*, ehr-*)
  • Accessibility enhancements beyond the base component’s ARIA pattern
clinical-confirm-button.ts
import { HxButton } from '@helixui/library';
customElements.define('clinical-confirm-button', class extends HxButton { ... });

Only extend when composing is not sufficient. Subclasses take on maintenance responsibility for compatibility with future HELiX releases. Configure and compose whenever possible.