Build responsive Home Assistant dashboards visually β drag, resize, layer, and arrange Lovelace cards exactly where you want them.
Start guide Β· Installation Β· Configuration Β· HADS Β· Releases
Drag & Drop Card is a freeform canvas for the Home Assistant Lovelace UI. Arrange any compatible card visually, create responsive layouts for different devices, save or share complete designs, and turn a dashboard into a polished full-screen experience.
New here? Follow the illustrated HADS start guide for the fastest path from installation to your first dashboard. This README is the detailed reference for configuration and advanced features.
- Features
- Installation
- Persistence backend
- Quick start
- Dashboard mode
- Configuration options
- LLM dashboard authoring
- Editor shortcuts
- Troubleshooting
- Contributing
- Support the project
- Visual editing: drag, resize, stack, multi-select, and snap Lovelace cards to a configurable grid.
- Responsive layouts: design for desktop, tablet, mobile, portrait, and landscape from the same dashboard.
- Tabs, layers, and Sidebar: organize large dashboards without giving up a free-positioned canvas.
- Fast editing workflow: auto-save or apply manually, use keyboard shortcuts, undo layout changes, and edit through the built-in toolbar.
- Portable designs: export and import complete JSON layouts, individual cards, options, responsive variants, and Home Assistant packages.
- Rich presentation: use images, particles, YouTube backgrounds, animated connectors, card entrance animations, and an optional screen saver.
- Home Assistant integration: add the card through the native card picker or register it as a discoverable community dashboard on supported Home Assistant versions.
- Design ecosystem: browse and import community designs from HADS and style the outer card with
card-mod. - Self-contained bundle:
interactjsandjs-yamlare bundled; no runtime CDN is required.
- In HACS, open the menu and choose Custom repositories.
- Add
https://github.com/Prosono/Drag-And-Drop-Cardwith Dashboard as the category. - Search for Drag & Drop Card, open it, and select Download.
- Reload Home Assistant and hard-refresh the browser if the card does not appear immediately.
The card is now available in Home Assistant's native card picker. On Home Assistant 2026.5 or newer, it also appears under Settings β Dashboards β Add dashboard β Community dashboards as Drag & Drop Dashboard.
- Download the compiled
dist/drag-and-drop-card.jsfile. - Copy it to
/config/www/drag-and-drop-card.js. - Add it under Settings β Dashboards β Resources, or add the Lovelace resource in YAML:
url: /local/drag-and-drop-card.js
type: moduleAfter adding a new resource, clear browser cache or hard-reload to ensure the module loads.
Recommended: Install the Drag & Drop Card Backend to keep editor changes in Home Assistant, make layouts available across browsers and devices, and sync optional packages.
Without the backend, the card falls back to browser localStorage. That is useful for testing, but the layout remains tied to that browser profile and can be lost when site data is cleared.
Add a Drag & Drop Card to your dashboard using YAML:
type: custom:drag-and-drop-card
storage_key: livingroom_layout # unique key per canvas
grid: 20 # pixel grid size (default editor stub)
drag_live_snap: true # snap while dragging/resizing
auto_save: true # auto-save after edits
auto_save_debounce: 800 # ms debounce
container_size_mode: auto # auto | fixed_custom | preset
container_background: transparent # canvas background
card_background: var(--ha-card-background, var(--card-background-color))
disable_overlap: false # prevent overlapping when true
background_mode: none # none | image | particles | youtube
debug: false # verbose console logsNow:
- Long-press on blank canvas (β1s) or double-click an empty area to enter Edit Mode.
- Use the toolbar to Add cards, Import/Export, Apply, or Exit edit mode.
- Ctrl/Cmd + S applies (saves) the layout while in edit mode.
- Esc exits edit mode.
Home Assistant 2026.5 introduced discoverable community dashboard strategies. This card registers a dashboard strategy named Drag & Drop Dashboard, so after the resource is loaded you can create a new dashboard from the Add dashboard dialog instead of starting from an empty dashboard and adding the card manually.
The generated dashboard contains a single panel view with one full-width Drag & Drop Card. You can also use the strategy in YAML:
strategy:
type: custom:drag-and-drop-card
title: Drag & Drop Dashboard
storage_key: my_drag_drop_dashboardOptional strategy settings:
| Key | Type | Description |
|---|---|---|
title |
string | Dashboard title used by the generated dashboard. |
storage_key |
string | Persistent Drag & Drop Card layout key. If omitted, one is derived from the dashboard details. |
view_title |
string | Title for the generated panel view. Defaults to Home. |
view_path |
string | URL path for the generated view. Defaults to home. |
view_icon |
string | Icon for the generated view. Defaults to mdi:home. |
card |
object | Extra Drag & Drop Card options merged into the generated card. |
You can define multiple tabs inside a single Drag & Drop card. Each tab has its own layout.
type: custom:drag-and-drop-card
storage_key: multi_tab_example
tabs:
- id: home
label: Home
icon: mdi:home
label_mode: both # icon | label | both
- id: media
label: Media
icon: mdi:television
label_mode: icon
default_tab: home
hide_tabs_when_single: true- The card remembers the last active tab per
storage_key. - Reorder tabs with the up/down controls in Dashboard Settings β Tabs.
- When there is only one tab and
hide_tabs_when_single: true, the tab bar is hidden.
Background behavior is controlled by background_mode:
none(default): no special background.image: usesbackground_image.particles: usesbackground_particles.youtube: usesbackground_youtube.
type: custom:drag-and-drop-card
storage_key: fancy_bg
background_mode: image
background_image:
src: /media/your/folder/background.png # or any HA-accessible URL
size: cover # cover | contain | 100% 100% | β¦
position: center center # CSS background-position
repeat: false # true | false | 'repeat'
opacity: 0.85 # 0β1
attachment: scroll # scroll | fixed
filter: blur(4px) brightness(0.8) # CSS filter() chainbackground_mode: particles
background_particles:
preset: default # implementation-specific; see docs/updates if provided
# Additional particle config may be supported in future versions.background_mode: youtube
background_youtube:
video_id: dQw4w9WgXcQ # YouTube video id
mute: true
loop: true
start: 0
end: 0 # 0 = entire video
size: cover # cover | contain | fill
attachment: fixed # fixed | scrollExact options for particles and YouTube are implementation-oriented; the above gives the general structure used by the card.
Optional screen saver that activates after inactivity:
screen_saver_enabled: true
screen_saver_delay: 300000 # milliseconds (e.g. 300000 = 5 minutes)
screen_saver_image: /local/screensaver.jpgWhen enabled, the card will enter a βscreen saverβ state after the delay. You can use the built-in visual presets or set screen_saver_image to replace the preset background with your own uploaded image, Home Assistant media URL, or external image URL.
Below is a summary of the main configuration options. Many have reasonable defaults and only need to be set when you want custom behavior.
| Key | Type | Default | Description |
|---|---|---|---|
storage_key |
string | auto | Unique ID for storing this canvasβ layout. If omitted, one is generated. |
grid |
number | 10 |
Grid size in px used for snapping and guides. New cards created via the stub start at 20. |
drag_live_snap |
boolean | false |
Snap while dragging/resizing (live feedback). |
auto_save |
boolean | true |
Automatically save changes. |
auto_save_debounce |
number | 800 |
Debounce window (ms) for auto-save. |
container_size_mode |
string | auto |
auto (responsive scaled canvas), fixed_custom, or preset. Legacy dynamic configs are automatically migrated to auto. |
container_fixed_width |
number | null |
Fixed width (px) when fixed_custom. |
container_fixed_height |
number | null |
Fixed height (px) when fixed_custom. |
container_preset |
string | fhd / fullhd |
Device/display preset key (see below) when preset. |
container_preset_orientation |
string | auto |
auto | portrait | landscape. |
auto_viewport_max_width |
number | 0 |
Caps the live Auto canvas viewport width in px. 0 or empty keeps the old unlimited behavior. |
auto_scale_max |
number | 0 |
Caps the live Auto canvas scale. 0 or empty keeps the old unlimited behavior. |
container_background |
string | transparent |
Canvas background (e.g. color/gradient). |
card_background |
string | var(--ha-card-background, var(--card-background-color)) |
Default background for wrapped cards. |
disable_overlap |
boolean | false |
If true, prevents overlapping during edit (experimental - NOT RECCOMENDED WHEN USING TABS!). |
animate_cards |
boolean | false |
If true, cards animate in when switching tabs or loading. |
background_mode |
string | none |
none | image | particles | youtube. |
background_image |
object | none | Image background settings when background_mode: image. |
background_particles |
object | none | Particle background settings when background_mode: particles. |
background_youtube |
object | none | YouTube background settings when background_mode: youtube. |
screen_saver_enabled |
boolean | false |
Enable screen saver mode. |
screen_saver_delay |
number | 300000 |
Screen saver delay in ms (fallback to 5 minutes if invalid). |
screen_saver_image |
string | none | Optional custom screen saver background image URL or uploaded data URL. |
tabs |
array | [] |
Tab definitions (see Tabs section). |
default_tab |
string | first tab id / 'default' |
Default tab id when the card loads. |
hide_tabs_when_single |
boolean | true |
Hide tab bar when there is only one tab. |
card_shadow |
boolean | false |
Apply a drop shadow to card wrappers. |
hide_HA_Header |
boolean | false |
Hide the Home Assistant top header while in this card. |
hide_HA_Sidebar |
boolean | false |
Hide the Home Assistant sidebar while in this card. |
edit_mode_pin |
string | '' |
Optional PIN required to enter edit mode (via supported UI). |
debug |
boolean | false |
Extra logging to the console. |
card_mod |
object | none | Card-mod config for the main card. |
cards |
array | none | Initial child cards (see below). |
- Phones:
iphone-14-pro,iphone-14-pro-max,iphone-se-2,pixel-7,galaxy-s8,galaxy-s20-ultra - Tablets:
ipad-9-7,ipad-11-pro,ipad-12-9-pro,surface-go-3 - Desktops:
hd,wxga-plus,fhd,qhd,ultrawide-uwqhd,uhd-4k
You can switch size mode at any time; the canvas will re-render accordingly.
Use the Add button in edit mode to pick from standard Lovelace cards or drag across an area to add a card directly to the grid.
Each added card is wrapped in a draggable/resizable container that participates in snapping and layout persistence.
- Layouts are saved per
storage_key. - Primary storage uses Home Assistantβs backend integration when available; otherwise falls back to
localStorage(ddc_local_<storage_key>) in the browser. - When backend becomes available, local layouts are migrated automatically.
- Auto-save is enabled by default; you can also use the Apply button or Ctrl/Cmd + S in edit mode for manual saves.
- Export produces a JSON file with version, options, and cards.
- Import reads JSON, applies
options(includingcard_mod, tab definitions, etc.), rebuilds the canvas, and keeps yourstorage_keyintact.
This section is written to make it easy for an LLM, agent, or automation tool to generate valid importable Drag & Drop Card dashboards without reverse-engineering the source code.
When generating a dashboard JSON for Drag & Drop Card, the model should think in five layers:
options: dashboard-level behavior and stylingcards: the desktop/base layout entriesresponsive_layouts: per-device or per-orientation variantsresponsive_connectorsinsideoptions: animated line overlays between itemspackages: optional Home Assistant YAML bundles synced by the backend
An importable dashboard JSON should follow this structure:
{
"version": 3,
"options": {},
"cards": [],
"responsive_layouts": {},
"packages": []
}{
"version": 3,
"options": {
"grid": 20,
"auto_save": true,
"container_size_mode": "auto",
"tabs": [
{ "id": "overview", "label": "Overview", "icon": "mdi:view-dashboard" }
],
"default_tab": "overview",
"hide_tabs_when_single": true
},
"cards": [
{
"id": "layout_card_title",
"card": {
"type": "custom:ddc-text-card",
"text": "Hello dashboard",
"variant": "title"
},
"position": { "x": 40, "y": 40 },
"size": { "width": 420, "height": 120 },
"z": 6,
"tabId": "overview"
}
],
"responsive_layouts": {
"desktop": {
"cards": [],
"landscape": { "cards": [] }
},
"tablet": {
"cards": [],
"landscape": { "cards": [] },
"portrait": { "cards": [] }
},
"mobile": {
"cards": [],
"landscape": { "cards": [] },
"portrait": { "cards": [] }
}
},
"packages": []
}Each dashboard item is stored as a layout entry:
{
"id": "layout_card_1",
"card": {
"type": "entities",
"title": "Example"
},
"position": { "x": 0, "y": 0 },
"size": { "width": 280, "height": 180 },
"z": 6,
"tabId": "overview",
"layerIds": ["standard"],
"card_style": {
"connector_anchors": "off"
},
"overflow": "visible"
}idshould be unique within the dashboard.positionandsizeare in canvas pixels.zshould usually start at6or higher.tabIdcontrols which tab the card belongs to.layerIdsis optional. If omitted, the card remains visible for backward compatibility.card_styleis optional and contains per-card design overrides. Setconnector_anchorstooffto hide that card's four editing anchors without removing existing connectors.
cards at the top level acts as the desktop/base layout.
responsive_layouts allows separate variants for:
desktop.landscapetablet.landscapetablet.portraitmobile.landscapemobile.portrait
Recommended shape:
{
"responsive_layouts": {
"desktop": {
"cards": [/* desktop landscape cards */],
"landscape": { "cards": [/* desktop landscape cards */] }
},
"tablet": {
"cards": [/* tablet landscape fallback */],
"landscape": { "cards": [/* tablet landscape cards */] },
"portrait": { "cards": [/* tablet portrait cards */] }
},
"mobile": {
"cards": [/* mobile landscape fallback */],
"landscape": { "cards": [/* mobile landscape cards */] },
"portrait": { "cards": [/* mobile portrait cards */] }
}
}
}- Treat portrait and landscape as separate layouts for
tabletandmobile. - If the same card exists in multiple profiles, keep the same
idbut allow differentpositionandsize. - New dashboards should usually define at least:
- one desktop layout
- one mobile portrait layout
- one tablet landscape layout
These are the most important option keys for an LLM to know:
| Key | Type | Meaning |
|---|---|---|
grid |
number | Snap size in px |
drag_live_snap |
boolean | Snap during drag/resize |
auto_save |
boolean | Save automatically |
auto_save_debounce |
number | Auto-save delay in ms |
container_size_mode |
string | auto, fixed_custom, preset |
auto_viewport_max_width |
number | Maximum live Auto viewport width in px; 0 means unlimited |
auto_scale_max |
number | Maximum live Auto scale; 0 means unlimited |
container_background |
string | Dashboard background color or gradient |
card_background |
string | Default wrapped card background |
card_shadow |
boolean | Enable default card shadows |
animate_cards |
boolean | Animate cards on tab/layer entry |
tabs |
array | Tab definitions |
tabs_position |
string | top, bottom |
sidebar_enabled |
boolean | Enable the independent Sidebar rail |
sidebar_items |
array | Sidebar content/order, e.g. navigation, weather, status, clock, date, profile |
sidebar_style |
string | glass, neon, minimal |
sidebar_density |
string | compact, comfortable, spacious |
sidebar_accent |
string | blue, cyan, purple, amber, green |
default_tab |
string | Default active tab id |
hide_tabs_when_single |
boolean | Hide tabs if only one exists |
layers_enabled |
boolean | Enable layer-based visibility |
layers |
array | Layer definitions |
background_mode |
string | none, image, particles, youtube |
background_image |
object | Image background settings |
background_particles |
object | Particle background settings |
background_youtube |
object | YouTube background settings |
dashboard_theme_enabled |
boolean | Enable Home Assistant theme styling |
dashboard_theme |
string | Theme name |
dashboard_theme_override_all_design |
boolean | Force theme to win over custom design choices |
responsive_viewports |
object | Editor preview sizes for desktop/tablet/mobile |
responsive_viewport_aspect_locks |
object | Per-profile preview ratio locks for desktop/tablet/mobile |
responsive_connectors |
object | Animated connector overlay layouts |
custom:ddc-html-card JavaScript can read and change dashboard-level settings through a local API exposed as ddc and helpers.ddc.
This API is intended for interactive dashboard controls such as buttons that enable/disable layers, screen saver, animations, tabs behavior, backgrounds, and other settings without opening the dashboard settings dialog.
Use the same option keys that appear in import/export JSON:
// Read a setting
const screenSaverOn = ddc.settings.get('screen_saver_enabled');
// Change a setting live for the current dashboard session
await ddc.settings.set('screen_saver_enabled', false);
// Persist the changed setting to storage/YAML when available
await ddc.settings.set('layers_enabled', true, { persist: true });
// Convenience helpers for boolean settings
await ddc.settings.enable('animate_cards');
await ddc.settings.disable('hide_tabs_when_single');
await ddc.settings.toggle('screen_saver_enabled');
// Change multiple settings at once
await ddc.settings.setMany({
screen_saver_enabled: true,
screen_saver_delay: 300000,
animate_cards: false
}, { persist: true });Available methods:
| Method | Purpose |
|---|---|
ddc.settings.list() |
List known settings with current values and inferred types |
ddc.settings.all() / ddc.settings.options() |
Return the current exportable dashboard options |
ddc.settings.get(key) |
Read one setting |
ddc.settings.set(key, value, options?) |
Apply one setting |
ddc.settings.setMany(patch, options?) |
Apply several settings |
ddc.settings.enable(key) / disable(key) / toggle(key) |
Boolean convenience helpers |
ddc.settings.save() |
Persist current settings |
ddc.settings.subscribe(handler) |
Listen for local ddc:settings-changed updates |
ddc.openSettings() |
Open the dashboard settings dialog |
ddc.saveLayout() |
Save the full layout |
By default, set, setMany, enable, disable, and toggle apply settings live only. Pass { "persist": true } when the change should be saved.
Example HTML card button:
{
"type": "custom:ddc-html-card",
"title": "Dashboard controls",
"html": "<button id='toggle-ss'>Toggle screen saver</button><span id='state'></span>",
"css": "button { padding: 10px 14px; border-radius: 12px; } #state { margin-left: 10px; }",
"js": "const btn = root.querySelector('#toggle-ss'); const label = root.querySelector('#state'); const render = () => { label.textContent = ddc.settings.get('screen_saver_enabled') ? 'On' : 'Off'; }; btn.addEventListener('click', async () => { await ddc.settings.toggle('screen_saver_enabled', { persist: true }); render(); }); const off = ddc.settings.subscribe(render); render(); return () => off();"
}Tabs are configured in options.tabs:
{
"tabs": [
{ "id": "overview", "label": "Overview", "icon": "mdi:view-dashboard" },
{ "id": "energy", "label": "Energy", "icon": "mdi:flash" },
{ "id": "automation", "label": "Automation", "icon": "mdi:robot" }
],
"default_tab": "overview",
"tabs_position": "top"
}Every card entry should then include a matching tabId.
Layers are independent of tabs and are used for toggling sets of cards inside the same tab:
{
"layers_enabled": true,
"layers": [
{ "id": "standard", "label": "Standard", "icon": "mdi:layers" },
{ "id": "energy", "label": "Energy", "icon": "mdi:flash" },
{ "id": "explainers", "label": "Explainers", "icon": "mdi:help-circle-outline" }
]
}Cards can then opt into one or more layers:
{
"layerIds": ["energy"]
}If a card has no layerIds, it should still remain visible. This is intentional and should be preserved when generating new dashboards that mix old and new content.
Lines are not authored as normal cards anymore. They are stored in options.responsive_connectors.
Recommended shape:
{
"responsive_connectors": {
"desktop": {
"connectors": [],
"landscape": { "connectors": [] }
},
"tablet": {
"connectors": [],
"landscape": { "connectors": [] },
"portrait": { "connectors": [] }
},
"mobile": {
"connectors": [],
"landscape": { "connectors": [] },
"portrait": { "connectors": [] }
}
}
}Each connector entry looks like this:
{
"id": "connector_power_1",
"tabId": "overview",
"cardIds": ["energy_card", "battery_card"],
"sourceCardId": "energy_card",
"targetCardId": "battery_card",
"layerIds": ["energy"],
"points": [
{ "x": 120, "y": 200 },
{ "x": 420, "y": 200 },
{ "x": 420, "y": 360 }
],
"entity": "input_boolean.ddc_demo_flow",
"active_states": "on,home,open,playing,charging,active,>0",
"arrows": "end",
"flow_direction": "auto",
"line_style": "dashed",
"thickness": 10,
"animate_mode": "active",
"animation_speed": 1.8,
"active_color": "var(--primary-color, #ff9800)",
"inactive_color": "rgba(148, 163, 184, 0.42)",
"glow": true,
"rounded": true
}pointsmust contain at least two points.- Points should snap to the same grid as the dashboard.
tabIdshould match the tab where the line is visible.cardIdsshould contain the unique layout card IDs the line belongs to.sourceCardIdandtargetCardIdshould match the first and last endpoint owners when the line connects two cards.layerIdsshould be included when the line belongs to specific layers; omit it only when the line should follow layer visibility from the owning card(s).- Use
entity+active_stateswhen the line should animate or change color based on Home Assistant state. - Connector anchors can be disabled for individual cards under Card Settings β Connector anchors. Saved connectors remain attached.
These are the custom cards an LLM can safely generate today.
Use this when the dashboard needs freeform HTML, CSS, and JavaScript inside a card.
{
"type": "custom:ddc-html-card",
"title": "Custom widget",
"html": "<div class='demo'>Hello</div>",
"css": ".demo { color: white; }",
"js": "return { update(){ /* read hass/states here */ } };",
"rerun_on_hass_update": false
}Runtime JavaScript receives access to:
hassstatesconfigroothosthelpersddc
The ddc object is the local dashboard API. For dashboard setting controls, prefer ddc.settings.get, ddc.settings.set, ddc.settings.toggle, ddc.settings.enable, and ddc.settings.disable.
Use this for titles, headings, paragraphs, small labels, and rich text.
{
"type": "custom:ddc-text-card",
"text": "Energy overview",
"rich_text": false,
"rich_html": "",
"variant": "title",
"font_family": "",
"font_size": 42,
"color": "var(--primary-text-color, #f8fafc)",
"align": "left",
"bold": true,
"italic": false,
"letter_spacing": -0.03,
"line_height": 1.05
}When rich_text is enabled, use rich_html as the canonical content.
Use this for pure design icons or state-driven status icons.
{
"type": "custom:ddc-icon-card",
"icon": "mdi:flash",
"entity": "input_boolean.ddc_demo_flow",
"size": 96,
"color": "var(--primary-color, #ff9800)",
"active_color": "#22c55e",
"state_based_color": true,
"glow": true,
"rotate": 0,
"pulse_when_active": true,
"opacity_based_on_state": false,
"active_opacity": 1,
"inactive_opacity": 0.28,
"active_states": "on,home,open,playing,charging,active,>0",
"click_action": "none"
}This card is intended to be visually transparent around the icon itself.
Use this for visual comparison tables, badges, and entity state summaries.
{
"type": "custom:ddc-table-card",
"title": "System matrix",
"rows": 3,
"columns": 3,
"header_row": true,
"border": true,
"radius": 16,
"spacing": 8,
"cells": [
{ "type": "text", "text": "Area", "align": "left" },
{ "type": "text", "text": "State", "align": "center" },
{ "type": "text", "text": "Action", "align": "center" },
{ "type": "icon", "icon": "mdi:flash", "text": "Power", "align": "left" },
{
"type": "entity",
"entity": "sensor.ddc_demo_status",
"text": "Live",
"align": "center",
"active_states": "on,active,>0",
"active_color": "var(--primary-color, #ff9800)",
"inactive_color": "rgba(148, 163, 184, 0.18)"
},
{
"type": "button",
"entity": "input_boolean.ddc_demo_flow",
"text": "Inspect",
"button_action": "more-info",
"align": "center"
}
]
}Supported cell types:
texticonentitybadgebutton
The dashboard can also contain normal Lovelace cards such as:
entitiesbuttontilegaugemarkdownglancesensorstatistics-graphhistory-graphpicture-entitypicture-glance
These should be placed in entry.card exactly as a normal Lovelace card config.
Packages are exported together with the dashboard and require the backend integration to sync into /config/packages.
Each package entry looks like this:
{
"id": "package_1",
"name": "Demo helpers",
"filename": "demo_helpers.yaml",
"enabled": true,
"yaml": "input_boolean:\\n ddc_demo_flow:\\n name: DDC Demo Flow\\n"
}- Only enabled packages with non-empty
yamlare written by the backend. - Package filenames are backend-normalized to a Home Assistant-safe slug.
- Home Assistant must have packages enabled in
configuration.yaml, for example:
homeassistant:
packages: !include_dir_named packagesThe editor supports exporting a single card and importing it into an existing dashboard.
When importing a single card:
- it is inserted into the current tab
- it is placed inside the active viewport
- it receives a new internal id
- it does not replace the whole dashboard
When generating a full demo dashboard, use this checklist:
- Define at least 2β3 tabs.
- If using layers, always include:
layers_enabled: true- a
standardlayer - at least one extra layer such as
energyorexplainers
- Place a mix of:
- standard HA cards
custom:ddc-text-cardcustom:ddc-icon-cardcustom:ddc-table-cardcustom:ddc-html-card
- Add at least one connector in
responsive_connectors. - For interactive dashboard controls, use
custom:ddc-html-cardwithddc.settings. - Keep card
tabIdand connectortabIdaligned, and bind connectors to the owning card IDs withcardIds. - If the dashboard should work on fresh installs, include demo
packages. - Prefer
mobile.portraitandtablet.landscapevariants instead of relying on desktop-only layout. - Keep
zvalues consistent and start at6.
For a strong showcase dashboard, include:
- one hero title using
custom:ddc-text-card - one live KPI icon using
custom:ddc-icon-card - one table summary using
custom:ddc-table-card - one interactive HTML widget using
custom:ddc-html-card - one Entities or Button card for controls
- one or more animated connectors
- at least one background mode
- one layers example and one tabs example
- one package that creates the helper entities used by the demo
Do not model connectors as normal cards in new dashboards. Use responsive_connectors instead. Legacy custom:ddc-line-card data may exist in older exports, but new dashboards should rely on the connector overlay system.
- Enter Edit: Long-press on blank canvas (~1s) or double-click an empty area.
- Exit Edit: Press Esc or use the Exit button.
- Apply: Ctrl/Cmd + S in edit mode or click Apply.
- Multi-select: Drag a selection marquee; use Shift/Ctrl/Cmd to extend selection.
- Small-card resize: Compact cards keep the bottom-right resize control visible while editing.
- Edit a card: Saving card code updates that card in place; the dashboard and active tab stay mounted.
- Toolbar:
- Add
- Reload
- Diagnostics
- Export / Import
- Apply layout (only needed when auto-save is off)
- Exit edit
The card supports card-mod on the outer drag-and-drop card.
Example to tweak grid color:
type: custom:drag-and-drop-card
storage_key: fancy_layout
card_mod:
style: |
:host {
--ddc-grid-color: rgba(255, 255, 255, 0.15);
}Example to make the container fully transparent while keeping inner cards untouched:
card_mod:
style: |
:host {
--ha-card-background: transparent;
border-radius: 18px;
overflow: hidden;
}
.ddc-root,
.card-container,
.layout,
.pane,
.rightGrid,
.section,
.toolbar,
.mdc-card,
.card,
.card *[class~="card"],
.mini,
.bd {
background: transparent !important;
box-shadow: none !important;
}Note: Only the main card supports
card_moddirectly. Inner cards should be styled via their owncard_modconfigs.
This card can browse and import community dashboard designs from HADS. Start with the HADS walkthrough if you want a guided setup.
For the deeper in-card marketplace integration, see the proposed HADS API contract:
- Module doesnβt load: Confirm resource URL & type. Hard-reload browser.
- Cards snap oddly: Adjust
gridor disabledisable_overlap. - Overlaps happen: Use
disable_overlap: true(experimental). - Layout didnβt persist: Ensure Apply or
auto_saveare used; checkstorage_keyand backend integration. - Imported design looks wrong: Check version & that the referenced cards exist in your system.
Click the preview to watch the latest release walkthrough.
For another hands-on preview, open the extended demo GIF (24 MB).
Issues, feature proposals, documentation improvements, and pull requests are welcome.
- Check the open issues before starting larger changes.
- Fork the repository and create a focused branch.
- Install dependencies, run the tests, and build the production bundle:
npm install
npm test
npm run build- Test the result in Home Assistant, including
card-modcompatibility when your change affects styling. - Open a pull request that explains what changed and how it was verified.
Please keep the project self-contained: runtime dependencies must be bundled, not loaded from an external CDN. By contributing, you agree that your contribution is provided under the project's MIT license.
If Drag & Drop Card saves you time, you can support the project by starring the repository, sharing your dashboards, reporting useful feedback, or buying me a coffee.
Released under the MIT License. Copyright (c) 2025 SMARTI AS.
Third-party licenses and bundled dependency notices are documented in THIRD_PARTY_NOTICES.md.
See GitHub Releases for version history, highlights, and upgrade notes. The installed bundle also logs its version in the browser console:
drag-and-drop-card vX.Y.Z
Known issues are tracked in the GitHub issue tracker. One current limitation is worth highlighting:
card-modsupport inside nested cards is still limited and may not behave as expected. The outer Drag & Drop Card supportscard_moddirectly.
