Show a live demo of your card
Embed working dashboard cards, including your own custom card, on any web page, with the mocked backend of the Home Assistant demo.
Screenshots show what a card looks like. A live demo shows how it works: people can turn on a light, move a slider, and see the other cards follow. The Home Assistant demo publishes elements that put real dashboard cards on any web page, like your README site or your blog. They run against a mocked backend in the browser, so no Home Assistant server is needed.
<ha-embed-provider>holds the mocked backend and its entities.<ha-embed-card>renders one card, with the backend of a provider.<ha-embed-section>renders one section, or a section strategy, the same way.<ha-embed-view>renders one view, a view strategy, or one view of a dashboard.<ha-embed-dashboard>renders a dashboard, or a dashboard strategy, with its tabs.
All cards in the same provider share their entities. When you change an entity in one card, the other cards show the change.
Try it here: the hallway tile and the thermostat card below it share one hallway thermostat. Change the target temperature in the tile or the mode in the thermostat card, and the other card follows. Each card has one card feature. The trend graph of the outside temperature is made up by the demo from its current state.
The components page of the demo shows all of this working, with every option.
Add the script of the demo to your page. It loads all its other files from the demo site too:
<script
type="module"
src="https://demo.home-assistant.io/embed.js"
></script>Put the entities in a script tag directly inside the provider, and the card configuration in a script tag directly inside the card. Both take JSON with type="application/json", or YAML with type="text/yaml". The card configuration is the same as in a dashboard.
<ha-embed-provider>
<script type="text/yaml">
- entity_id: light.kitchen
state: "on"
attributes:
friendly_name: Kitchen
supported_color_modes:
- brightness
color_mode: brightness
brightness: 180
</script>
<ha-embed-card>
<script type="text/yaml">
type: tile
entity: light.kitchen
features:
- type: light-brightness
</script>
</ha-embed-card>
</ha-embed-provider>Each entity has an entity_id, a state, and attributes. The optional keys area_id, device_id, and platform set its entry in the entity registry. Without last_changed, the entity changed at a random recent time.
Entities keep all their attributes, like in Home Assistant, so attributes that only your integration sets are there too. Entities with a specialized mock, including lights, climate, covers, people, and device trackers, work out some attributes themselves, and those win over what you set: a light that is off has no brightness, for example.
YAML needs its indentation. If your site minifies HTML, make sure the minifier keeps the whitespace in script tags, or use JSON. The entities attribute of the provider and the config attribute of the card take JSON too.
Load the script of your card on the page, after the script of the demo, and use its custom: type, the same as in a dashboard:
<script
type="module"
src="https://demo.home-assistant.io/embed.js"
></script>
<script
type="module"
src="https://example.com/my-card.js"
></script>
<ha-embed-card>
<script type="text/yaml">
type: custom:my-card
entity: light.kitchen
</script>
</ha-embed-card>Your card gets the same mocked hass object as the built-in cards, and window.loadCardHelpers() works for the card helpers. The card helpers only exist once the script of the demo has run. Module scripts run in the order they are on the page, so keep the script of your card after it, in case your card uses them as soon as its script loads. Many built-in cards, like the map card, only load when a card first needs them, so create built-in cards through the card helpers instead of by their element name. When the script of your card is on another site than your page, that site needs to send CORS headers for it.
A custom strategy loads the same way. Load its script after the script of the demo, and use its custom: type in the strategy of a section, view, or dashboard.
<ha-embed-section> renders a section the same way as in a sections view. Its configuration is the same as in a dashboard: a title becomes a heading card when there is no strategy, headings span the top, and a strategy generates the section. The section takes the width of the element. This example uses the provider from Add a card, with a switch.kettle added to it:
<ha-embed-section>
<script type="text/yaml">
cards:
- type: heading
heading: Kitchen
- type: tile
entity: light.kitchen
- type: tile
entity: switch.kettle
</script>
</ha-embed-section>Try it here: a section with a heading, two tiles you can turn on and off, and a wide tile below them. The ceiling light belongs to a device in devices.
A section strategy that groups entities by device or area needs those in the provider. See Areas, devices, calendars, and energy.
<ha-embed-view> renders a view, with its badges and layout, and a strategy generates the view. Give the element the width the view needs: masonry and sidebar views pick their number of columns from the width of the page, not of the element. This example uses the provider from Add a card:
<ha-embed-view>
<script type="text/yaml">
type: sections
badges:
- entity: light.kitchen
sections:
- cards:
- type: tile
entity: light.kitchen
</script>
</ha-embed-view>Try it here: a sections view with a badge for the outside temperature, and two sections side by side when there is room.
A view strategy and a dashboard strategy both use the strategy key. To show one view of a dashboard, or of a dashboard strategy, put the dashboard in dashboard and the path or index of the view in view. Without view, the first view shows.
dashboard:
strategy:
type: custom:my-dashboard
view: overview<ha-embed-dashboard> renders a whole dashboard with its tabs, and a strategy generates the dashboard. The dashboard scrolls inside the element, under its toolbar. The element is 600 pixels high; set its height to change that. To start on another view than the first, set its view attribute to the path or index of that view. This example uses the provider from Add a card:
<ha-embed-dashboard style="height: 480px">
<script type="text/yaml">
views:
- title: Home
path: home
cards:
- type: tile
entity: light.kitchen
- title: Kitchen
path: kitchen
cards:
- type: light
entity: light.kitchen
</script>
</ha-embed-dashboard>Try it here: a dashboard with two tabs. Switch between them, or select the hallway tile on the first tab to go to the second.
The toolbar only shows the tabs, or the title of the view, and the back button of a subview. There is no edit mode. Each dashboard keeps its own view, so the address of your page stays the same, and two dashboards on one page don't follow each other.
Links in the dashboard work like this:
- A link to one of the views of the dashboard, like
navigation_path: climateor/my-dashboard/climate, shows that view. - The back button of a subview goes to its
back_pathwhen that is a view of this dashboard. Otherwise it goes to the view before it, or the first view when there is none, so aback_pathto another page does not leave the demo. - A link to an anchor on your page, like
#setup, scrolls the page to it. - Other
navigateactions do nothing. Aurlaction opens its page, the same as in a dashboard.
On a page with several demos, or with a demo far down the page, you can wait to load the script of the demo until a demo comes near, so the rest of the page loads first. The elements read their script tags when they are set up, so the cards still find their entities and configuration when the script comes later. Watch every card, section, view, and dashboard on the page, then import the script of the demo first, and the script of your card after it:
const loadDemo = async () => {
await import("https://demo.home-assistant.io/embed.js");
await import("https://example.com/my-card.js");
};
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
observer.disconnect();
loadDemo();
}
},
{ rootMargin: "400px 0px" },
);
for (const demo of document.querySelectorAll(
"ha-embed-card, ha-embed-section, ha-embed-view, ha-embed-dashboard",
)) {
observer.observe(demo);
}Cards, sections, views, and dashboards inside a provider count too. querySelectorAll does not look inside shadow roots, so for demos inside your own components, observe those components instead. It does not watch providers: a provider at the top of the page would load the demo right away, even when its cards are far below. Run it from a module script, so the elements of the page are there when it looks for them. Leave out the script tags of the demo and your card when you load them this way.
A card does not have to be inside a provider. A card outside a provider uses the first provider on the page. A card inside one of your own components uses the provider around that component, even through its shadow root. Put one provider with all entities at the top of the page, and the cards anywhere below it, between your text. If the page has no provider, the card adds an empty one. Sections, views, and dashboards find their provider the same way, and take the same provider attribute.
To use another provider, set the provider attribute of the card to the ID of that provider:
<ha-embed-provider id="living-room">…</ha-embed-provider>
<ha-embed-card provider="living-room">…</ha-embed-card>Set provider before the card is added to the page. The card picks its provider when it is added, and does not follow a later change.
If you maintain several cards or strategies, you don't need a demo site for each. Give each project a demo config, with the entities in entities and what to show in cards, sections, views, or dashboard. The configs can all live in the host site, or each project can keep its own, like a demo.yml next to its code:
entities:
- entity_id: light.kitchen
state: "on"
attributes:
friendly_name: Kitchen
cards:
- type: custom:my-card
entity: light.kitchenThe site then renders each config: the entities go into a provider, and each card, section, view, or dashboard into its element, as YAML in their script tags. Give each project its own provider, so their entities stay apart, and load the script of each card after the script of the demo.
The cards follow the light or dark mode of the browser. When your site has its own switch for dark mode, set the color-scheme attribute of the provider to light or dark. The cards keep their styles inside, and do not change the styles of your page. The one exception is the Roboto font: the script adds its font faces to the page, so text of your page that uses Roboto can look different once the script has loaded.
Some cards need data that is not an entity. To set it, put an object in the script tag of the provider instead of the list. The object has the list in entities, and any of these keys:
areas: a list of areas, each with anarea_idand aname. Without it, eacharea_idof the entities and devices becomes an area.devices: a list of devices, each with anidand aname, and optionally amanufacturer, amodel, and anarea_id. An entity belongs to a device through itsdevice_id, so cards and strategies find the device and its name.calendars: the events of calendar entities, by entity ID. Each event has asummary, astart, and anend. Calendars that are not listed here get sample events around today. A calendar with an empty list has no events.energy: the energy settings, the keys you need from theenergy/get_prefsresponse. Without it, energy is not set up.
entities:
- entity_id: calendar.family
state: "off"
attributes:
friendly_name: Family
- entity_id: light.kitchen_ceiling
state: "on"
device_id: kitchen_ceiling
attributes:
friendly_name: Ceiling
areas:
- area_id: kitchen
name: Kitchen
devices:
- id: kitchen_ceiling
name: Kitchen ceiling light
manufacturer: Acme
model: Smart bulb
area_id: kitchen
calendars:
calendar.family:
- summary: Dentist
start: "2026-11-02T15:30:00+00:00"
end: "2026-11-02T16:15:00+00:00"Calendar cards only show the events in the dates they display, so change these dates to ones near the day of your demo. To always have events, leave the calendar out of calendars and use the sample events. They are set around the day the script of the demo loads, and loading the page again sets them again.
Weather entities get a forecast made up from their condition and temperature, or from their forecast attribute when they have one. Set supported_features to show forecasts.
To change one entity, update it in provider.hass.mockEntities. The other entities stay as they are, and so does what the entity remembers, like the brightness of a light that is off. Run it from a module script, after the provider has set up its entities. This turns off the kitchen light from Add a card:
await customElements.whenDefined("ha-embed-provider");
const provider = document.querySelector("ha-embed-provider");
await provider.updateComplete;
provider.hass.mockEntities["light.kitchen"].update({ state: "off" });To use an action of an entity, call it in the same module script. The provider updates the state and the cards:
provider.hass.callService(
"light",
"turn_on",
{ brightness_pct: 50 },
{ entity_id: "light.kitchen" },
);Setting the entities property of the provider replaces all entities, and they start again from what you set.
The provider fires a states-changed event when the state of one or more entities changes. Its detail has states, the states of all entities by entity ID.
The mocked backend only knows a part of the Home Assistant API. Keep these limits in mind when you pick what to show:
- The actions of lights, switches, climate, covers, fans, locks, media players, and more change the state of the entity.
- Actions only reach entities that they target by entity ID. An action that targets an area or a device does nothing.
- History and statistics are made up from the current state of each entity. A few energy sensors of the demo, like
sensor.solar_production, get made-up statistics of their own instead, so their state does not change their graph. - Templates are not rendered. Template subscriptions fill in simple
{{ variable }}references from the variables you pass and return the rest as it is. The RESTtemplateendpoint returns an error. - The logbook is empty.
- The
toggle,perform-action, andurlactions of a card work. Actions that open a dialog, likemore-info, do nothing. An action with aconfirmationdoes nothing, because it waits for a dialog that does not open, unless the confirmation exempts the demo user. - A
navigateaction to an anchor on your page, like#setup, scrolls there. A dashboard also follows links to its own views, as Show a dashboard describes, and so does a view given adashboard, as in Show a view. Othernavigateactions do nothing. - Actions the backend does not handle, or for entities it does not have, do nothing. They do not return an error.
- Other requests that wait for an answer, over WebSocket or REST, get an error.
The mocks live in demo/src/stubs/ of the frontend repository. If your card needs a request they do not know, see the gallery and the demo for where to add it.