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.
- About this fork
- Requirements
- Installation
- Visual Editor
- Configuration
- Custom CSS
- Examples
- Migration from the original
stack-in-card
This project is a modernised complete rewrite of the 2020-era custom-cards/stack-in-card, rebuilt to support the latest Home Assistant frontend stack (HA 2025.1+).
For all new features, bug fixes, and improvements, please check the CHANGELOG.md.
- 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 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.
Scope, precisely. The CSS is written into every shadow root inside that child, so it reaches the card's own
ha-cardwherever it mounts β and stays inside that card. Shadow DOM does the isolating, not the selector.One exception: a card that keeps its
ha-cardoutside any shadow DOM. There is nowhere private to put the CSS, so it goes into the scope the child shares with its siblings β and then it can affect them, whatever the selector,ha-card { }included. Almost every card has a shadow DOM, so this is rare, but it is why per-child CSS cannot promise isolation unconditionally.A card that renders neither a shadow DOM nor an
ha-cardreceives no per-child CSS at all β there is nothing to aim at, and the previous catch-all styled the entire stack. If you hit this, please open an issue naming the card.A child that is itself a
custom:stack-in-cardis special: the field on it is that card's own stack CSS, applied to itsha-cardalone. To style the cards inside it, use their ownstack_in_card_styles.
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.
-
Putting back what the card strips needs
!important.background,box-shadowandborder-radiusare removed from child cards by writing them as inline styles, and an inline style outranks a normal rule. Measured:border-radius: 12px !importantin a child's CSS box gives 12px, without!importantit stays 0.The matching HA theme variable is no help here either:
--ha-card-border-radius: 12pxdoes nothing, and not because of this card β measured on a live instance, setting it on the card, on its wrapper, or at theme level all leave the radius untouched. Write the property itself:ha-card { border-radius: 12px !important; }
If you want the property back on every child rather than one, use the
keepoptions instead β that skips the stripping altogether and leaves the theme variables working. -
The spacing between cards is the exception: it is set through HA's own
--stack-card-gap, so no!importantgymnastics βha-card { --stack-card-gap: 2px !important }in the stack card's CSS box just works, and so does any other value. -
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 β with the caveat above for the three stripped properties. -
Copying or cutting a child card in the editor puts its full config β including the
stack_in_card_stylesfield β onto Home Assistant's shared clipboard. Pasting it back into another stack-in-card keeps the CSS. Pasting it into a plain dashboard card works too, but some cards with strict config validation may reject the extrastack_in_card_styleskey; just delete that key if a paste target complains.
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 typecheck
npm test # vitest (jsdom)Build output is a single ES module at dist/stack-in-card.js (~48 KB minified).