diff --git a/gallery/src/data/provide_hass.js b/gallery/src/data/provide_hass.js
index 79db10cbda..8d260a2213 100644
--- a/gallery/src/data/provide_hass.js
+++ b/gallery/src/data/provide_hass.js
@@ -29,6 +29,12 @@ export default (elements, { initialStates = {} } = {}) => {
resources: demoResources,
states: initialStates,
themes: {},
+ connection: {
+ subscribeEvents: async (callback, event) => {
+ console.log("subscribeEvents", event);
+ return () => console.log("unsubscribeEvents", event);
+ },
+ },
// Mock properties
mockEntities: entities,
diff --git a/gallery/src/demos/demo-hui-shopping-list-card.js b/gallery/src/demos/demo-hui-shopping-list-card.js
new file mode 100644
index 0000000000..2f485dfcc0
--- /dev/null
+++ b/gallery/src/demos/demo-hui-shopping-list-card.js
@@ -0,0 +1,55 @@
+import { html } from "@polymer/polymer/lib/utils/html-tag";
+import { PolymerElement } from "@polymer/polymer/polymer-element";
+
+import provideHass from "../data/provide_hass";
+import "../components/demo-cards";
+
+const CONFIGS = [
+ {
+ heading: "List example",
+ config: `
+- type: shopping-list
+ `,
+ },
+ {
+ heading: "List with title example",
+ config: `
+- type: shopping-list
+ title: Shopping List
+ `,
+ },
+];
+
+class DemoShoppingListEntity extends PolymerElement {
+ static get template() {
+ return html`
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ _configs: {
+ type: Object,
+ value: CONFIGS,
+ },
+ };
+ }
+
+ ready() {
+ super.ready();
+ const hass = provideHass(this.$.demos);
+
+ hass.mockAPI("shopping_list", () => [
+ { name: "list", id: 1, complete: false },
+ { name: "all", id: 2, complete: false },
+ { name: "the", id: 3, complete: false },
+ { name: "things", id: 4, complete: true },
+ ]);
+ }
+}
+
+customElements.define("demo-hui-shopping-list-card", DemoShoppingListEntity);
diff --git a/src/data/shopping-list.ts b/src/data/shopping-list.ts
new file mode 100644
index 0000000000..9766354e21
--- /dev/null
+++ b/src/data/shopping-list.ts
@@ -0,0 +1,28 @@
+import { HomeAssistant } from "../types";
+
+export interface ShoppingListItem {
+ id: number;
+ name: string;
+ complete: boolean;
+}
+
+export const fetchItems = (hass: HomeAssistant): Promise =>
+ hass.callApi("GET", "shopping_list");
+
+export const saveEdit = (
+ hass: HomeAssistant,
+ itemId: number,
+ name: string
+): Promise =>
+ hass.callApi("POST", "shopping_list/item/" + itemId, {
+ name,
+ });
+
+export const completeItem = (
+ hass: HomeAssistant,
+ itemId: number,
+ complete: boolean
+): Promise =>
+ hass.callApi("POST", "shopping_list/item/" + itemId, {
+ complete,
+ });
diff --git a/src/panels/lovelace/cards/hui-shopping-list-card.ts b/src/panels/lovelace/cards/hui-shopping-list-card.ts
new file mode 100644
index 0000000000..ac918c856d
--- /dev/null
+++ b/src/panels/lovelace/cards/hui-shopping-list-card.ts
@@ -0,0 +1,165 @@
+import { html, LitElement } from "@polymer/lit-element";
+import { repeat } from "lit-html/directives/repeat";
+import { TemplateResult } from "lit-html";
+import "@polymer/paper-checkbox/paper-checkbox";
+import "@polymer/paper-input/paper-input";
+
+import "../../../components/ha-card";
+
+import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
+import { HomeAssistant } from "../../../types";
+import { LovelaceCard, LovelaceConfig } from "../types";
+import {
+ fetchItems,
+ completeItem,
+ saveEdit,
+ ShoppingListItem,
+} from "../../../data/shopping-list";
+
+interface Config extends LovelaceConfig {
+ title?: string;
+}
+
+class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
+ implements LovelaceCard {
+ private _hass?: HomeAssistant;
+ private _config?: Config;
+ private _items?: ShoppingListItem[];
+ private _unsubEvents?: Promise<() => Promise>;
+
+ static get properties() {
+ return {
+ _config: {},
+ _items: {},
+ };
+ }
+
+ set hass(hass: HomeAssistant) {
+ this._hass = hass;
+ }
+
+ public getCardSize(): number {
+ return (
+ (this._config ? (this._config.title ? 1 : 0) : 0) +
+ (this._items ? this._items.length : 3)
+ );
+ }
+
+ public setConfig(config: Config): void {
+ this._config = config;
+ this._items = [];
+ this._fetchData();
+ }
+
+ public connectedCallback(): void {
+ super.connectedCallback();
+
+ if (this._hass) {
+ this._unsubEvents = this._hass.connection.subscribeEvents(
+ () => this._fetchData(),
+ "shopping_list_updated"
+ );
+ this._fetchData();
+ }
+ }
+
+ public disconnectedCallback(): void {
+ super.disconnectedCallback();
+
+ if (this._unsubEvents) {
+ this._unsubEvents.then((unsub) => unsub());
+ }
+ }
+
+ protected render(): TemplateResult {
+ if (!this._config || !this._hass) {
+ return html``;
+ }
+
+ return html`
+ ${this.renderStyle()}
+
+ ${repeat(
+ this._items!,
+ (item) => item.id,
+ (item, index) =>
+ html`
+
+ `
+ )}
+
+ `;
+ }
+
+ private renderStyle(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private async _fetchData(): Promise {
+ if (this._hass) {
+ this._items = await fetchItems(this._hass);
+ }
+ }
+
+ private _completeItem(ev): void {
+ completeItem(this._hass!, ev.target.itemId, ev.target.checked).catch(() =>
+ this._fetchData()
+ );
+ }
+
+ private _saveEdit(ev): void {
+ saveEdit(this._hass!, ev.target.itemId, ev.target.value).catch(() =>
+ this._fetchData()
+ );
+
+ ev.target.blur();
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-shopping-list-card": HuiShoppingListCard;
+ }
+}
+
+customElements.define("hui-shopping-list-card", HuiShoppingListCard);
diff --git a/src/panels/lovelace/common/create-card-element.js b/src/panels/lovelace/common/create-card-element.js
index f7cc0db4a3..c4cbc406a2 100644
--- a/src/panels/lovelace/common/create-card-element.js
+++ b/src/panels/lovelace/common/create-card-element.js
@@ -21,6 +21,7 @@ import "../cards/hui-picture-glance-card";
import "../cards/hui-plant-status-card";
import "../cards/hui-sensor-card";
import "../cards/hui-vertical-stack-card.ts";
+import "../cards/hui-shopping-list-card";
import "../cards/hui-thermostat-card.ts";
import "../cards/hui-weather-forecast-card";
import "../cards/hui-gauge-card";
@@ -49,6 +50,7 @@ const CARD_TYPES = new Set([
"picture-glance",
"plant-status",
"sensor",
+ "shopping-list",
"thermostat",
"vertical-stack",
"weather-forecast",