Skip to content
HELiX

Styling Components with CSS Parts

apps/docs/src/content/docs/extending/style-components-with-css-parts Click to copy
Copied! apps/docs/src/content/docs/extending/style-components-with-css-parts

CSS ::part() selectors are the second extension vector in HELiX after CSS custom properties. They give you direct styling access to named elements inside a component’s shadow DOM — without modifying component source, without shadowRoot.querySelector(), and without coupling your styles to undocumented internal class names.

This guide covers the complete catalog of exposed parts per component category, compound selector patterns, when to use parts versus tokens, and real-world recipes for the patterns healthcare teams encounter most.


The ::part() pseudo-element, defined in the CSS Shadow Parts specification, allows you to pierce a shadow root and style a specific element that the component author has exposed with a part attribute.

/* From outside the shadow DOM: */
hx-button::part(button) {
letter-spacing: 0.06em;
}
/* Inside hx-button's shadow DOM, the author has written: */
/* <button part="button" class="button">...</button> */

The part attribute is an explicit, versioned API contract. Every part listed in this guide is declared in the Custom Elements Manifest and covered by HELiX’s semantic versioning guarantee — they will not be renamed or removed without a major version bump.

Parts vs CSS Custom Properties: The Decision Rule

Section titled “Parts vs CSS Custom Properties: The Decision Rule”

Both mechanisms style across the shadow boundary. Choose based on what you are changing:

Change typeUseWhy
Brand color, font family, spacing scaleCSS custom property (--hx-*)One override propagates everywhere via the token cascade
Component-specific color, border radiusComponent CSS property (--hx-button-bg)Scoped override, still participates in the cascade
Hover gradient, box-shadow animation, text-transform::part()Properties that cannot be expressed as a single token value
Layout changes (flexbox direction, grid columns)::part()Structural properties live outside the token system
Pseudo-class interaction states (:hover, :focus-visible)::part() with pseudo-classThe only mechanism for interaction styling from outside shadow DOM
Adding decorations (:before, :after)Not possible via ::part()Pseudo-elements cannot be applied to ::part() targets

The practical rule: reach for a CSS custom property first. If no token covers the change, or if you need interaction states tied to shadow-internal element behavior, use ::part().


The tables below cover the most commonly styled parts on each component. Each entry shows the part name, the element it targets, and typical styling use cases — but the listing is curated, not exhaustive. For the authoritative inventory (including parts not surfaced here for brevity), consult packages/hx-library/custom-elements.json or each component’s per-component docs page.

PartElementTypical use
buttonThe native <button> or <a> elementBackground, border, shadow, hover effects, text-transform
labelThe label text wrapper <span>Typography overrides, letter-spacing
prefixThe prefix slot container <span>Icon spacing, sizing
suffixThe suffix slot container <span>Icon spacing, sizing
spinnerThe loading spinner SVGSpinner size, color
PartElementTypical use
buttonThe native <button> or <a> elementBackground, border, shape
iconThe icon container <span>Icon sizing
PartElementTypical use
buttonThe primary action button elementPrimary segment styling
triggerThe dropdown trigger button elementTrigger segment styling
menuThe dropdown menu panelMenu panel appearance
PartElementTypical use
buttonThe native button elementToggle button appearance
labelThe label text wrapperTypography
PartElementTypical use
buttonThe copy-to-clipboard buttonButton appearance
PartElementTypical use
fieldThe outer field container <div>Field layout, spacing
labelThe <label> elementLabel typography, color, positioning
input-wrapperWrapper around prefix, input, and suffixBorder, background, focus ring on wrapper
inputThe native <input> elementInput typography, padding, background
help-textThe help text containerHelp text styling
errorThe error message containerError state styling
PartElementTypical use
fieldThe outer field containerField layout
labelThe <label> elementLabel styling
textarea-wrapperThe wrapper around the textareaBorder, background
textareaThe native <textarea> elementTextarea appearance, min-height
counterThe character count displayCounter positioning, typography
help-textThe help text containerHelp text styling
errorThe error message containerError state styling
PartElementTypical use
fieldThe outer field containerField layout
labelThe <label> elementLabel styling
input-wrapperWrapper around prefix, input, suffix, and stepperWrapper appearance
inputThe native <input> elementInput appearance
stepperThe stepper button group containerStepper layout
incrementThe increment (+) buttonIncrement button styling
decrementThe decrement (-) buttonDecrement button styling
help-textThe help text containerHelp text styling
errorThe error message containerError state styling
PartElementTypical use
fieldThe outer field containerField layout
labelThe <label> elementLabel styling
select-wrapperThe wrapper containing the trigger and listboxWrapper appearance
triggerThe button that opens/closes the dropdownTrigger button styling
listboxThe dropdown panel containing optionsListbox appearance, z-index
optionIndividual option itemsOption item styling
help-textThe help text containerHelp text styling
errorThe error message containerError state styling
PartElementTypical use
controlThe wrapper around checkbox and labelControl alignment
checkboxThe visual checkbox elementCheckbox size, border, background
checkmarkThe SVG checkmark iconCheckmark color, size
labelThe label elementLabel typography
help-textThe help text containerHelp text styling
errorThe error message containerError styling
PartElementTypical use
radioThe visual radio circleRadio size, border, fill color
labelThe label textLabel typography
PartElementTypical use
switchThe switch container (track and thumb wrapper)Overall switch sizing
trackThe track background elementTrack color, border-radius
thumbThe sliding thumb elementThumb color, size, shadow
labelThe label text elementLabel typography
help-textThe help text containerHelp text styling
errorThe error message containerError state styling
PartElementTypical use
cardThe outer card container <div>Background, border, shadow, border-radius
imageThe image slot containerImage area sizing, overflow
headingThe heading slot containerHeading area padding, typography
bodyThe body slot containerBody padding, typography
footerThe footer slot containerFooter padding, border
actionsThe actions slot containerActions area layout, border
PartElementTypical use
dialogThe inner container <div> holding dialog contentDialog dimensions, background
backdropThe non-modal backdrop overlayBackdrop color, blur
headerThe header regionHeader padding, border-bottom
close-buttonThe built-in close buttonClose button appearance
bodyThe scrollable body regionBody padding, max-height
footerThe footer regionFooter padding, border-top, alignment
PartElementTypical use
overlayThe full-screen overlay containerOverlay behavior
panelThe drawer panel itselfPanel width, background, shadow
headerThe header regionHeader padding, border
titleThe drawer title elementTitle typography
close-buttonThe built-in close buttonClose button appearance
bodyThe scrollable body regionBody padding
footerThe footer regionFooter padding, border
PartElementTypical use
itemThe outer <details> elementItem border, background
triggerThe <summary> trigger elementTrigger padding, typography, hover
contentThe collapsible content areaContent padding
iconThe expand/collapse iconIcon color, size
PartElementTypical use
headerThe outer <header> landmark elementHeader background, border, height
navThe <nav> element inside the headerNav layout
logoThe logo slot containerLogo sizing, spacing
menuThe primary navigation slot containerMenu alignment
actionsThe actions slot containerActions alignment
mobile-toggleThe hamburger toggle buttonToggle button appearance
PartElementTypical use
navThe outer <nav> elementNav width, background
headerThe header sectionHeader padding
bodyThe scrollable body sectionBody scrolling, padding
footerThe footer sectionFooter padding
toggleThe collapse/expand toggle buttonToggle button appearance
PartElementTypical use
linkThe anchor or button elementLink padding, background, color
iconThe icon containerIcon sizing
labelThe label containerLabel typography
badgeThe badge containerBadge positioning
childrenThe children containerNested item indentation
PartElementTypical use
tablistThe tablist container elementTab bar background, border
panelsThe panel content container elementPanel padding, background
PartElementTypical use
tabThe underlying <button> elementTab padding, typography, active state
prefixThe prefix slot content containerIcon spacing
suffixThe suffix slot content containerBadge spacing
PartElementTypical use
tableThe <table> elementTable border-collapse, width
theadThe <thead> elementHeader background
tbodyThe <tbody> elementBody striping
trEach <tr> elementRow hover, border
thEach <th> elementHeader cell typography, padding
tdEach <td> elementBody cell padding
sort-iconThe sort indicator icon <span>Sort indicator styling
checkboxEach <input type="checkbox">Row selection checkbox
PartElementTypical use
trackThe outer track containerTrack height, border-radius, background
fillThe filled portion indicating progressFill color, animation
labelThe label slot wrapperLabel typography
PartElementTypical use
alertThe outer alert containerAlert background, border, border-radius
iconThe icon containerIcon color, size
titleThe title/headline containerTitle typography
messageThe message content areaMessage typography
close-buttonThe dismiss buttonClose button appearance
actionsThe actions containerActions layout
PartElementTypical use
badgeThe badge elementBadge shape, padding, typography
remove-buttonThe remove/dismiss buttonRemove button sizing
PartElementTypical use
tooltipThe tooltip container elementTooltip background, padding, border-radius
arrowThe arrow indicator elementArrow color, size
PartElementTypical use
avatarThe outer container elementAvatar size, shape
imageThe <img> elementImage object-fit
initialsThe initials text spanInitials typography, color
fallback-iconThe SVG person silhouetteFallback icon styling
badgeThe badge slot containerBadge positioning

::part() composes with other CSS selectors to target interaction states, attribute conditions, and component variants.

Scope a part override to a specific component variant using attribute selectors on the host element:

/* Only affects danger variant buttons */
hx-button[variant='danger']::part(button) {
font-weight: 800;
letter-spacing: 0.04em;
}
/* Only affects disabled text inputs */
hx-text-input[disabled]::part(input-wrapper) {
opacity: 0.5;
cursor: not-allowed;
}
/* Only affects compact cards */
hx-card[variant='compact']::part(body) {
padding: var(--hx-space-3);
}

Combine ::part() with pseudo-classes to style hover, focus, and active states:

/* Hover state on the inner button element */
hx-button::part(button):hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
/* Focus-visible on the native input — preserves keyboard accessibility */
hx-text-input::part(input):focus-visible {
outline: 3px solid var(--hx-input-focus-ring-color);
outline-offset: 2px;
}
/* Active state for tactile feedback */
hx-button::part(button):active {
transform: translateY(0);
box-shadow: none;
}
/* Tab selection state */
hx-tab[selected]::part(tab) {
border-bottom: 3px solid var(--hx-color-primary-500);
}

Scope part overrides to a page region using a context class on an ancestor:

/* Clinical vitals panel: larger, bolder badges */
.patient-vitals-panel hx-badge::part(badge) {
font-size: 1rem;
font-weight: 700;
padding: var(--hx-space-1) var(--hx-space-3);
}
/* High-contrast mode for ICU displays */
.high-contrast hx-button::part(button) {
border: 2px solid currentColor;
font-weight: 700;
}
/* Compact layout for data-dense views */
.compact-mode hx-text-input::part(field) {
gap: var(--hx-space-1);
}

Use native pseudo-classes like :focus-within, :hover, and :active on the host element together with ::part() to style internal regions in response to host state. (HELiX components don’t currently expose CSS CustomStateSet states — the :state(...) selector is not part of the public API.)

/* Style the wrapper when the input inside is focused */
hx-text-input:focus-within::part(input-wrapper) {
border-color: var(--hx-color-primary-500);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* Style the card when focused (interactive card pattern) */
hx-card:focus-within::part(card) {
outline: 2px solid var(--hx-color-primary-500);
outline-offset: 2px;
}

CSS :has() cannot reach inside shadow DOM — you cannot write hx-text-input:has(input:invalid). However, :has() on the host works when the host reflects the relevant attribute:

/* Works: hx-text-input reflects [disabled] to the host */
hx-text-input:has(+ hx-help-text)::part(field) {
margin-bottom: 0;
}
/* hx-text-input does not reflect its `error` property to a host attribute, so
`hx-text-input[error]` only matches when consumers (or their templates) set
the `error` HTML attribute explicitly. If you want the selector to track
the JS property, set the attribute on the host alongside the property. */
hx-text-input[error]::part(input-wrapper) {
border-color: var(--hx-input-error-color);
}

See Limitations and Workarounds for a full treatment.


CSS Custom Properties and Parts Working Together

Section titled “CSS Custom Properties and Parts Working Together”

The most powerful patterns combine both mechanisms: tokens for the baseline visual system, parts for structural or decorative changes that tokens cannot express.

Pattern: Token for Color, Part for Structure

Section titled “Pattern: Token for Color, Part for Structure”
/* Token override at the semantic/action layer changes primary-painted buttons
across the whole page. `--hx-button-bg` is variant-shadowed inside hx-button —
`variant="primary"`/`secondary` rules resolve their fill directly from the
action tokens, so override there to reach every variant. */
hx-button {
--hx-color-action-primary-bg: var(--hx-color-primary-700);
--hx-color-action-primary-bg-hover: var(--hx-color-primary-800);
}
/* Part override: adds structural decoration the token system cannot */
hx-button::part(button) {
text-transform: uppercase;
letter-spacing: 0.08em;
border-radius: 0; /* Clinical UI often uses square corners */
}

Pattern: Token for Theme, Part for Interaction

Section titled “Pattern: Token for Theme, Part for Interaction”
/* Set the base theme via tokens */
hx-text-input {
--hx-input-border-color: var(--hx-color-neutral-400);
--hx-input-focus-ring-color: var(--hx-color-primary-500);
}
/* Add interaction effects via parts */
hx-text-input::part(input-wrapper) {
transition:
border-color 150ms ease,
box-shadow 150ms ease;
}
hx-text-input:focus-within::part(input-wrapper) {
border-color: var(--hx-color-primary-500);
box-shadow: 0 0 0 3px rgba(0, 94, 184, 0.15); /* NHS brand blue focus ring */
}
/* Tokens drive the color system */
hx-card {
--hx-card-bg: var(--hx-color-surface-raised);
--hx-card-border-color: transparent;
}
/* Part adds the shadow (not expressible as a single token) */
hx-card::part(card) {
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.08);
transition: box-shadow 200ms ease;
}
hx-card:hover::part(card) {
box-shadow:
0 4px 6px rgba(0, 0, 0, 0.1),
0 2px 4px rgba(0, 0, 0, 0.08);
}

Recipe 1: Custom Hover Gradient on hx-button

Section titled “Recipe 1: Custom Hover Gradient on hx-button”

A branded primary button with a gradient background and smooth hover lift — common for call-to-action buttons in patient portals.

/* Brand gradient button */
hx-button[variant='primary']::part(button) {
background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%);
border: none;
transition:
opacity 200ms ease,
transform 150ms ease,
box-shadow 200ms ease;
}
hx-button[variant='primary']::part(button):hover {
opacity: 0.92;
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.35);
}
hx-button[variant='primary']::part(button):active {
opacity: 1;
transform: translateY(0);
box-shadow: none;
}
/* Ensure focus ring remains visible over the gradient */
hx-button[variant='primary']::part(button):focus-visible {
outline: 3px solid #7c3aed;
outline-offset: 3px;
}
/* Loading state: maintain gradient but show spinner clearly */
hx-button[loading]::part(spinner) {
color: rgba(255, 255, 255, 0.9);
}

Accessibility note: The :focus-visible rule is required. The default focus ring from the token system may not have sufficient contrast against a dark gradient. Always verify contrast with your specific colors.

A more pronounced focus treatment for clinical forms where accurate data entry is critical.

/* Smooth border and shadow transition */
hx-text-input::part(input-wrapper) {
transition:
border-color 150ms ease,
box-shadow 150ms ease;
}
/* Strong focus ring for clinical accuracy */
hx-text-input:focus-within::part(input-wrapper) {
border-color: var(--hx-color-primary-500);
box-shadow:
0 0 0 3px rgba(37, 99, 235, 0.12),
0 1px 3px rgba(0, 0, 0, 0.08);
}
/* Error state: override to red focus ring when invalid */
hx-text-input[error]:focus-within::part(input-wrapper) {
border-color: var(--hx-input-error-color);
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12);
}
/* Required field indicator styling */
hx-text-input[required]::part(label) {
font-weight: 600;
}

Recipe 3: Card Hover Elevation for Interactive Cards

Section titled “Recipe 3: Card Hover Elevation for Interactive Cards”

Patient summary cards in a list that lift on hover to indicate interactivity.

/* Base: minimal shadow */
hx-card[hx-href]::part(card) {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
transition:
box-shadow 200ms ease,
transform 200ms ease;
cursor: pointer;
}
/* Hover: elevation lift */
hx-card[hx-href]:hover::part(card) {
box-shadow:
0 4px 6px rgba(0, 0, 0, 0.1),
0 2px 4px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
/* Focus: visible ring for keyboard navigation */
hx-card[hx-href]:focus-within::part(card) {
outline: 3px solid var(--hx-color-primary-500);
outline-offset: 2px;
box-shadow:
0 4px 6px rgba(0, 0, 0, 0.1),
0 2px 4px rgba(0, 0, 0, 0.08);
}
/* Active: depress on click */
hx-card[hx-href]:active::part(card) {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}

Enhanced alerts for a medication management interface with custom left-border treatment.

/* Base: add left border accent for all alerts */
hx-alert::part(alert) {
border-left: 4px solid transparent;
border-radius: 0 var(--hx-border-radius-md) var(--hx-border-radius-md) 0;
}
/* Variant-specific border colors via host attribute */
hx-alert[variant='info']::part(alert) {
border-left-color: var(--hx-color-info-500);
}
hx-alert[variant='success']::part(alert) {
border-left-color: var(--hx-color-success-500);
}
hx-alert[variant='warning']::part(alert) {
border-left-color: var(--hx-color-warning-500);
}
hx-alert[variant='error']::part(alert) {
border-left-color: var(--hx-color-error-500);
}
/* Clinical urgency: larger title for critical alerts */
hx-alert[variant='error']::part(title) {
font-size: 1.0625rem;
font-weight: 700;
}

An hx-top-nav styled for a healthcare brand with a dark header and logo area.

/* Dark header background */
hx-top-nav::part(header) {
background: var(--hx-color-neutral-900);
border-bottom: 1px solid var(--hx-color-neutral-700);
color: var(--hx-color-neutral-0);
}
/* Logo area: fixed width for visual stability */
hx-top-nav::part(logo) {
min-width: 160px;
}
/* Mobile toggle: white icon on dark background */
hx-top-nav::part(mobile-toggle) {
color: var(--hx-color-neutral-0);
}
hx-top-nav::part(mobile-toggle):hover {
background: rgba(255, 255, 255, 0.1);
}

::slotted() is a component-author selector — it lives inside a component’s own shadow stylesheet and matches light-DOM nodes that have been projected into the component’s slots. Consumers writing CSS in the light DOM don’t reach into a component’s shadow tree with ::slotted(); they style their own light-DOM nodes directly with normal selectors, and use ::part() for the shadow container around the slot.

/*
* Consumer light-DOM CSS:
* - Style your slotted children with normal selectors (`hx-card > [slot='heading']`).
* - Style the shadow container around the slot with `::part()`.
*/
/* The light-DOM heading content you projected into the card */
hx-card > [slot='heading'] {
font-size: var(--hx-font-size-lg);
font-weight: var(--hx-font-weight-semibold);
color: var(--hx-color-neutral-900);
margin: 0;
}
/* Action buttons nested in the actions slot — direct child selector only;
you can't reach into a nested component's shadow DOM from here. */
hx-card > [slot='actions'] hx-button {
/* …light-DOM-side styling… */
}
/* Combine: style the shadow container via ::part() and the projected
light-DOM nodes with ordinary selectors. */
hx-card::part(heading) {
padding-bottom: var(--hx-space-2);
border-bottom: 1px solid var(--hx-color-neutral-100);
}
hx-card > [slot='heading'] {
font-size: 1.125rem;
font-weight: 600;
}

Important: ::slotted() is only valid inside the shadow stylesheet of the component doing the slotting (i.e. the library author’s CSS). From the consumer side, use direct light-DOM selectors plus ::part().


Understanding ::part() limitations prevents architectural dead-ends before you encounter them.

You cannot write hx-button::part(button) span or hx-button::part(button) > .icon. The ::part() pseudo-element matches the part element itself only — no descendants.

Why: The shadow boundary is still in effect after ::part(). You are styling one element, not unlocking the entire shadow subtree.

Workarounds:

/* DOES NOT WORK — no descendant selector after ::part() */
hx-button::part(button) span {
color: red;
}
/* WORKS — component exposes a separate part for inner elements */
hx-button::part(label) {
color: red;
}
/* WORKS — if no part exists, request one via a GitHub issue,
or use a CSS custom property if the component exposes one */
hx-button {
--hx-button-color: red;
}

If you find yourself needing to style an element inside a part that has no exposed part of its own, file an issue requesting that the component author expose a new named part.

You cannot apply ::before or ::after to a ::part() target. The CSS specification explicitly prohibits this.

/* DOES NOT WORK */
hx-button::part(button)::before {
content: '';
}
/* WORKAROUNDS: */
/* Option A: Use the prefix slot for content injection */
/* HTML: <hx-button><span slot="prefix">→</span>Submit</hx-button> */
/* Option B: Use a CSS custom property if the component supports it */
/* Option C: Wrap in a container and style the container */
.my-button-wrapper {
position: relative;
}
.my-button-wrapper::before {
content: '';
position: absolute;
left: -1.5em;
}

Limitation 3: :has() Cannot Reach Inside Shadow DOM

Section titled “Limitation 3: :has() Cannot Reach Inside Shadow DOM”

The CSS :has() pseudo-class cannot select based on elements inside a shadow root:

/* DOES NOT WORK — can't query shadow internals */
hx-text-input:has(input:invalid)::part(input-wrapper) {
border-color: red;
}
/* WORKS — the component reflects its error state to the host */
/* Set the error attribute or property on the host element */
hx-text-input[error]::part(input-wrapper) {
border-color: var(--hx-input-error-color);
}
/* WORKS — use :focus-within which fires on the host */
hx-text-input:focus-within::part(input-wrapper) {
border-color: var(--hx-color-primary-500);
}

HELiX components reflect relevant states as host attributes (e.g., [disabled], [required], [error] via re-render, [loading]) specifically to enable this workaround pattern.

Limitation 4: Dynamic Part Names Are Not Supported

Section titled “Limitation 4: Dynamic Part Names Are Not Supported”

CSS ::part() requires static, known part names at authoring time. You cannot generate part names dynamically in CSS.

/* DOES NOT WORK */
hx-button::part(var(--dynamic-part-name)) {
}

This is a fundamental constraint of the specification. Design your part selectors statically.

Limitation 5: exportparts Required for Nested Custom Elements

Section titled “Limitation 5: exportparts Required for Nested Custom Elements”

If you build a wrapper component that uses HELiX components internally, the inner component’s parts are not automatically reachable from outside your wrapper’s shadow DOM.

// my-clinical-button wraps hx-button internally
// From outside my-clinical-button, ::part(button) does NOT reach
// hx-button's inner button part
// Solution: use exportparts in the wrapper's template
// <hx-button exportparts="button, label, prefix, suffix"></hx-button>

When building wrapper components, use exportparts to forward the parts you want consumers to be able to style.


CSS Shadow Parts (::part()) has broad support across all modern browsers:

BrowserSupportVersion
Chrome / ChromiumFull support73+
Edge (Chromium)Full support79+
FirefoxFull support72+
Safari / WebKitFull support13.1+
Samsung InternetFull support10.1+

For HELiX’s supported browser baseline — see BROWSER_COMPATIBILITY.md for the authoritative matrix; current targets are Chromium/Edge/Firefox 120+ and Safari/Safari iOS 17+ — ::part() can be used without any polyfill or fallback.

Note on :has() compatibility: CSS :has() used on the host element (not inside ::part()) requires Chrome 105+, Firefox 121+, Safari 15.4+. For healthcare deployments targeting older browser versions, test :has() selectors with your actual browser matrix before committing to them in production.