A modernised replacement for vertical-stack-in-card and
horizontal-stack-in-card. Group multiple Lovelace cards into a single
seamless card β without inner borders, shadows or padding gaps. Includes a
HA-native visual editor and per-card custom CSS.
This is a complete rewrite of the 2020-era stack-in-card@0.2.0 on a
current Home Assistant frontend stack (HA 2025.1+).
- β‘ Lit 3 + TypeScript 5.7 + Rollup 4 β full migration from Lit-element 2 / TS 4 / Rollup 2
- π¦ No
custom-card-helpersβ local types, modern HAloadCardHelpers()-only path - π Style-application via
MutationObserverβ replaces the fragilesetTimeout(500)of the original; late-mounted nested cards (e.g.mushroom,button-card) get their styles applied as they mount - π Race-condition guarded β monotonic generation counter on async stack creation; rapid config updates from the editor can no longer overwrite each other
Matches HA's own stack-card editor 1:1:
- ποΈ Native
<ha-tab-group>tabs for switching between child cards (falls back to styled buttons on HA versions without it) - ποΈ Action row β GUI/YAML toggle, RTL-aware move-before / move-after, copy, cut, delete (same icons + German translations as HA's built-in editor)
- β Embedded
<hui-card-picker>for adding cards β full picker UX with built-in, custom, and HACS cards. Used inline, noshow-create-card-dialoground-trip - π Paste-from-clipboard banner β shares HA's
dashboardCardClipboardsessionStorage key, so cards copied from any HA editor (built-in stack, this card, etc.) can be pasted here - π§© Embedded
<hui-card-element-editor>per child β the same nested editor HA uses; auto-detects per-type GUI editors, falls back to YAML for types that don't ship one. Re-mounted on every reorder via Lit'skeyed()directive (same mechanism HA itself uses) so reorder actually persists in YAML - πͺ Empty stack opens directly in the picker β adding the card drops the user straight into card selection, no placeholder children to delete first
- πͺ§ Inline empty-state placeholder β never a spinning preview in the picker
- π¨ Top-level
stack_in_card_stylesβ CSS applied to the outer stack card (ha-cardwrapper) - π― Per-child
cards[i].stack_in_card_stylesβ CSS injected into a single child's shadow DOM only (no leak to siblings) - π‘οΈ Namespaced field name β avoids clashing with cards like
bubble-card/button-cardthat use their ownstyles:field - βοΈ Both edited via
<ha-code-editor>with entity / icon autocompletion in the visual editor - π§³ Per-child styles travel with the card across reorder / copy / paste
- π Element double-registration guard β loading the bundle twice (HACS + manual resource) no longer throws
NotSupportedError - β
Card validation in
setConfigβ rejects unknownmodevalues and non-arraycards - π§Ή Cleanup on disconnect β animation frames, mutation observer, and card promise all torn down in
disconnectedCallback - π
customCards.typewithoutcustom:prefix β HA's picker callsdocument.createElement(type)on this value; the prefix would fail silently and hang the preview tile
- π¦ Mutation-burst debounce β live-updating children (
history-graph,mini-graph-card, animations) no longer pin the main thread on repeated style re-walks; bursts are coalesced into a single pass every ~150 ms - π― Mutation filter β observer ignores text / attribute changes and only reacts to actual element insertions
- πΌοΈ SVG-namespace filter β SVG child elements (
<path>,<animate>,<g>, β¦) added by graphing cards (mini-graph-card,apexcharts-card) are excluded from the observer entirely; they can never introduce a newha-cardto strip, so reacting to them was pure overhead - π³ Linear O(N) DOM walker β
walkShadowAndLightpreviously mixedquerySelectorAll('*')with recursive child traversal, visiting shadow-DOM descendants once per ancestor level (quadratic growth). Now uses only direct.childrenper level with a visited guard; every node is touched exactly once - β±οΈ CSS-injection retry cap tightened β 3 Γ 200 ms instead of 10 Γ 500 ms; stuck children no longer spam
walkShadowAndLightfor 5 seconds - π§Ή CSS-injection retry cancellation β pending retries are cancelled when the stack is rebuilt or disconnected, preventing stale CSS from leaking into new card structures
- πͺ Picker null-deref fix β
<hui-card-picker>is now unmounted on the next animation frame after a pick, so its ownupdated()pass finishes cleanly (no moregetElementById on nullathui-card-picker.ts:286)
- π¦
dist/stack-in-card.jsshipped in master β HACS finds the built file directly, no manual release required for installation - π€ Auto-build on push β
.github/workflows/build.ymltypechecks, builds, and commitsdist/back to master - π·οΈ Tagged-release workflow β
git tag v2.x.x && git push --tagsbuilds and creates a GitHub Release with the JS file as an asset - βοΈ HACS validation workflow β verifies the repo stays HACS-compliant on every push
For the full version history see CHANGELOG.md.
- What's different
- Requirements
- Installation
- Visual Editor
- Configuration
- Custom CSS
- Examples
- Migration from the original
stack-in-card
- Home Assistant 2025.1 or newer
(some editor features β
<ha-tab-group>,<hui-card-element-editor>,@mdi/jsicon paths β rely on frontend changes from late 2024 / early 2025. On HA 2025.10+ the editor uses the native tab control; on older versions it falls back to styled buttons.) - HACS (recommended) or manual install
- Open HACS in Home Assistant
- Go to Frontend β three-dot menu β Custom repositories
- Add this repository URL, category: Lovelace
- Search for Stack In Card and install
- Reload the browser (hard refresh: Ctrl+Shift+R)
- Download
stack-in-card.jsfrom the latest release (or fromdist/stack-in-card.json master) and place it inconfig/www/. - Add to your Lovelace resources:
resources:
- url: /local/stack-in-card.js?v=1
type: moduleThe card has a built-in visual editor accessible from the HA card picker. Most settings can be configured without YAML.
| Section | What it does |
|---|---|
| Title | Optional header text rendered at the top of the stack |
| Mode | vertical (default) or horizontal layout of the child cards |
| Keep options | Toggle which visual properties of child cards to preserve (background, box-shadow, border-radius, margin between cards, outer padding) |
| Custom CSS β Stack card | CSS code editor for the outer <ha-card> wrapper |
| Cards | Tab strip + add (+) button; click + to open the embedded card picker |
| Per-card actions | GUI/YAML toggle, move before, move after, copy, cut, delete β same as HA's own stack editor |
| Custom CSS β Card N | CSS code editor for the currently selected child card (scoped to that child only) |
Tip: Cards copied or cut in any HA editor (this card, HA's built-in stack, etc.) appear as a Paste from clipboard entry at the top of the picker β they share the same
dashboardCardClipboardsessionStorage slot.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
type |
string | yes | custom:stack-in-card |
|
title |
string | no | Header of the wrapper card | |
mode |
string | no | vertical or horizontal |
vertical |
cards |
array | yes | Child card configs (each may carry its own stack_in_card_styles field) |
[] |
keep |
object | no | See keep object | |
stack_in_card_styles |
string | no | CSS applied to the outer stack card (the ha-card wrapper) |
| Name | Type | Description | Default |
|---|---|---|---|
background |
boolean | Keep the background on all child cards. To keep it only on specific ones, use data-keep-background="true" on that card's ha-card element (fast O(1) lookup), or set the CSS variable --keep-background: 'true' on that card (backwards-compatible, requires getComputedStyle). |
false |
box_shadow |
boolean | Keep the box-shadow on all child cards. |
false |
margin |
boolean | Keep the margin between all child cards. |
false |
outer_padding |
boolean | Add 8px padding around the inner stack when margin is kept. |
true if margin is true, otherwise false |
border_radius |
boolean | Keep the border-radius on all child cards. |
false |
Two levels:
Lives at the top level of the config as stack_in_card_styles. Applied to the outer <ha-card> wrapper itself.
type: custom:stack-in-card
stack_in_card_styles: |
ha-card {
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
cards:
- type: entities
entities: [sun.sun]Lives on each child's own config as stack_in_card_styles. Injected into that child's shadow DOM only β doesn't leak to siblings.
type: custom:stack-in-card
cards:
- type: button
entity: sun.sun
stack_in_card_styles: |
ha-card {
background: linear-gradient(135deg, #ff9966 0%, #ff5e62 100%) !important;
}
- type: button
entity: sun.sun
stack_in_card_styles: |
ha-card {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important;
}Because per-child CSS lives on the child config, it travels with the card across reorder / copy / paste. No parallel index array to keep in sync.
π‘ Why the long name? A plain
styles:would clash with cards likebubble-cardandbutton-cardthat use the same key for their own styling system. The namespacedstack_in_card_styles:is unmistakably ours and lets both systems coexist on the same child.
backgroundneeds!importantto override HA's--ha-card-backgroundtheme variable.border-radius,box-shadow, and most other properties usually don't.- The visual editor's CSS editors use
mode="yaml"because HA's<ha-code-editor>doesn't ship a CSS mode. Highlighting won't perfectly match CSS, but everything works. - Prefer HA's CSS variables (
--primary-color,--card-background-color, etc.) so your stack respects the active theme.
Useful for button-card, which colours its own ha-card:
type: custom:stack-in-card
mode: vertical
cards:
- type: custom:button-card
entity: sun.sun
color_type: card
styles:
card:
- --keep-background: 'true'The --keep-background CSS variable is read by the stack itself before deciding whether to strip the child's background. Alternatively, you can set data-keep-background="true" directly on the ha-card element β this is an O(1) attribute lookup and slightly faster than the CSS-variable path.
type: custom:stack-in-card
mode: vertical
stack_in_card_styles: |
ha-card {
border-radius: 20px;
box-shadow: 0 8px 24px rgba(255, 94, 98, 0.4);
}
cards:
- type: entities
entities:
- sun.sun
stack_in_card_styles: |
ha-card {
background: linear-gradient(135deg, #ff9966 0%, #ff5e62 100%) !important;
color: white !important;
}type: custom:stack-in-card
stack_in_card_styles: |
ha-card {
background: rgba(255, 255, 255, 0.08) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15) !important;
border-radius: 16px;
}
cards:
- type: entities
entities:
- sun.sunAll 0.2.x YAML configurations continue to work unchanged. The only addition is the optional stack_in_card_styles field β either on the top-level config (for the stack card) or on individual child configs.
Behavioural differences worth knowing:
- The editor is now visual by default β the + button opens HA's embedded card picker rather than requiring YAML edits.
- An empty
cards: []is now a valid config (renders an empty-state placeholder); the original threw on this. - Per-child styles live on each child's config (
cards[i].stack_in_card_styles), not on a separate index-keyed array.
npm install
npm run dev # rollup watch
npm run build # production build β dist/stack-in-card.js
npm run typecheckBuild output is a single ES module at dist/stack-in-card.js (~48 KB minified).