Repository: home-assistant/frontend
Pull request #25555: Save sidebar in user data
Author: login: wendevlin
Labels: name: cla-signed
name: Breaking Change
description: Needs Special Documentation to let users know about Change
name: Noteworthy
description: Marks a PR as noteworthy and should be in the release notes (in case it normally would not appear)
Created: 2025-05-21T18:51:24Z | Merged: 2025-05-26T13:25:11Z

## Breaking change
- Sidebar order and hidden panels will be stored in user data instead of local storage. Sidebar will stay the same on each device as long as there is no user data available.
  - `sidebarHiddenPanels`
  - `sidebarPanelOrder`

## Proposed change
 

## Type of change
<!--
  What type of change does your PR introduce to the Home Assistant frontend?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (thank you!)
- [x] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Example configuration
<!--
  Supplying a configuration snippet, makes it easier for a maintainer to test
  your PR.
-->

```yaml

```

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #
- This PR is related to issue or discussion:
- Link to documentation pull request:

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [ ] The code change is tested and works locally.
- [ ] There is no commented out code in this PR.
- [ ] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

<!--
  Thank you for contributing <3
-->

[docs-repository]: https://github.com/home-assistant/home-assistant.io



Diff:
diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts
index 64476ae81a5e..d2f77a977061 100644
--- a/src/components/ha-sidebar.ts
+++ b/src/components/ha-sidebar.ts
@@ -14,7 +14,6 @@ import {
   mdiTooltipAccount,
   mdiViewDashboard,
 } from "@mdi/js";
-import type { UnsubscribeFunc } from "home-assistant-js-websocket";
 import type { CSSResultGroup, PropertyValues } from "lit";
 import { LitElement, css, html, nothing } from "lit";
 import {
@@ -30,6 +29,7 @@ import { fireEvent } from "../common/dom/fire_event";
 import { toggleAttribute } from "../common/dom/toggle_attribute";
 import { stringCompare } from "../common/string/compare";
 import { throttle } from "../common/util/throttle";
+import { subscribeFrontendUserData } from "../data/frontend";
 import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
 import type { PersistentNotification } from "../data/persistent_notification";
 import { subscribeNotifications } from "../data/persistent_notification";
@@ -41,11 +41,13 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
 import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
 import { haStyleScrollbar } from "../resources/styles";
 import type { HomeAssistant, PanelInfo, Route } from "../types";
+import "./ha-fade-in";
 import "./ha-icon";
 import "./ha-icon-button";
 import "./ha-md-list";
 import "./ha-md-list-item";
 import type { HaMdListItem } from "./ha-md-list-item";
+import "./ha-spinner";
 import "./ha-svg-icon";
 import "./user/ha-user-badge";
 
@@ -187,38 +189,57 @@ class HaSidebar extends SubscribeMixin(LitElement) {
   @property({ attribute: "always-expand", type: Boolean })
   public alwaysExpand = false;
 
-  @property({ attribute: false })
-  public panelOrder!: string[];
-
-  @property({ attribute: false })
-  public hiddenPanels!: string[];
-
   @state() private _notifications?: PersistentNotification[];
 
   @state() private _updatesCount = 0;
 
   @state() private _issuesCount = 0;
 
+  @state() private _panelOrder?: string[];
+
+  @state() private _hiddenPanels?: string[];
+
   private _mouseLeaveTimeout?: number;
 
   private _tooltipHideTimeout?: number;
 
   private _recentKeydownActiveUntil = 0;
 
-  private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
-
   @query(".tooltip") private _tooltip!: HTMLDivElement;
 
-  public hassSubscribe(): UnsubscribeFunc[] {
-    return this.hass.user?.is_admin
-      ? [
-          subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
-            this._issuesCount = repairs.issues.filter(
-              (issue) => !issue.ignored
-            ).length;
-          }),
-        ]
-      : [];
+  public hassSubscribe() {
+    return [
+      subscribeFrontendUserData(
+        this.hass.connection,
+        "sidebar",
+        ({ value }) => {
+          this._panelOrder = value?.panelOrder;
+          this._hiddenPanels = value?.hiddenPanels;
+
+          // fallback to old localStorage values
+          if (!this._panelOrder) {
+            const storedOrder = localStorage.getItem("sidebarPanelOrder");
+            this._panelOrder = storedOrder ? JSON.parse(storedOrder) : [];
+          }
+          if (!this._hiddenPanels) {
+            const storedHidden = localStorage.getItem("sidebarHiddenPanels");
+            this._hiddenPanels = storedHidden ? JSON.parse(storedHidden) : [];
+          }
+        }
+      ),
+      subscribeNotifications(this.hass.connection, (notifications) => {
+        this._notifications = notifications;
+      }),
+      ...(this.hass.user?.is_admin
+        ? [
+            subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
+              this._issuesCount = repairs.issues.filter(
+                (issue) => !issue.ignored
+              ).length;
+            }),
+          ]
+        : []),
+    ];
   }
 
   protected render() {
@@ -254,8 +275,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
       changedProps.has("_updatesCount") ||
       changedProps.has("_issuesCount") ||
       changedProps.has("_notifications") ||
-      changedProps.has("hiddenPanels") ||
-      changedProps.has("panelOrder")
+      changedProps.has("_hiddenPanels") ||
+      changedProps.has("_panelOrder")
     ) {
       return true;
     }
@@ -279,23 +300,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
     );
   }
 
-  protected firstUpdated(changedProps: PropertyValues) {
-    super.firstUpdated(changedProps);
-    this._subscribePersistentNotifications();
-  }
-
-  private _subscribePersistentNotifications(): void {
-    if (this._unsubPersistentNotifications) {
-      this._unsubPersistentNotifications();
-    }
-    this._unsubPersistentNotifications = subscribeNotifications(
-      this.hass.connection,
-      (notifications) => {
-        this._notifications = notifications;
-      }
-    );
-  }
-
   protected updated(changedProps) {
     super.updated(changedProps);
     if (changedProps.has("alwaysExpand")) {
@@ -307,14 +311,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 
     const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
 
-    if (
-      this.hass &&
-      oldHass?.connected === false &&
-      this.hass.connected === true
-    ) {
-      this._subscribePersistentNotifications();
-    }
-
     this._calculateCounts();
 
     if (!SUPPORT_SCROLL_IF_NEEDED) {
@@ -369,11 +365,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
   }
 
   private _renderAllPanels(selectedPanel: string) {
+    if (!this._panelOrder || !this._hiddenPanels) {
+      return html`
+        <ha-fade-in .delay=${500}
+          ><ha-spinner size="large"></ha-spinner
+        ></ha-fade-in>
+      `;
+    }
+
     const [beforeSpacer, afterSpacer] = computePanels(
       this.hass.panels,
       this.hass.defaultPanel,
-      this.panelOrder,
-      this.hiddenPanels,
+      this._panelOrder,
+      this._hiddenPanels,
       this.hass.locale
     );
 
@@ -559,18 +563,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
       return;
     }
 
-    showEditSidebarDialog(this, {
-      saveCallback: this._saveSidebar,
-    });
+    showEditSidebarDialog(this);
   }
 
-  private _saveSidebar = (order: string[], hidden: string[]) => {
-    fireEvent(this, "hass-edit-sidebar", {
-      order,
-      hidden,
-    });
-  };
-
   private _itemMouseEnter(ev: MouseEvent) {
     // On keypresses on the listbox, we're going to ignore mouse enter events
     // for 100ms so that we ignore it when pressing down arrow scrolls the
@@ -730,13 +725,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
           display: none;
         }
 
+        ha-fade-in,
         ha-md-list {
-          padding: 4px 0;
-          box-sizing: border-box;
-          height: calc(100% - var(--header-height) - 132px);
           height: calc(
             100% - var(--header-height) - 132px - var(--safe-area-inset-bottom)
           );
+        }
+
+        ha-fade-in {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+        }
+
+        ha-md-list {
+          padding: 4px 0;
+          box-sizing: border-box;
           overflow-x: hidden;
           background: none;
           margin-left: var(--safe-area-inset-left);
diff --git a/src/data/frontend.ts b/src/data/frontend.ts
index 7e46217ccaba..8f0c6749cc1b 100644
--- a/src/data/frontend.ts
+++ b/src/data/frontend.ts
@@ -5,9 +5,15 @@ export interface CoreFrontendUserData {
   showEntityIdPicker?: boolean;
 }
 
+export interface SidebarFrontendUserData {
+  panelOrder: string[];
+  hiddenPanels: string[];
+}
+
 declare global {
   interface FrontendUserData {
     core: CoreFrontendUserData;
+    sidebar: SidebarFrontendUserData;
   }
 }
 
diff --git a/src/data/translation.ts b/src/data/translation.ts
index 260294e50600..88ffff8ee864 100644
--- a/src/data/translation.ts
+++ b/src/data/translation.ts
@@ -1,9 +1,5 @@
 import type { HomeAssistant } from "../types";
-import {
-  fetchFrontendUserData,
-  saveFrontendUserData,
-  subscribeFrontendUserData,
-} from "./frontend";
+import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend";
 
 export enum NumberFormat {
   language = "language",
@@ -78,9 +74,6 @@ export type TranslationCategory =
   | "selector"
   | "services";
 
-export const fetchTranslationPreferences = (hass: HomeAssistant) =>
-  fetchFrontendUserData(hass.connection, "language");
-
 export const subscribeTranslationPreferences = (
   hass: HomeAssistant,
   callback: (data: { value: FrontendLocaleData | null }) => void
diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts
index efc42284ccde..fed10ad59337 100644
--- a/src/dialogs/sidebar/dialog-edit-sidebar.ts
+++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts
@@ -1,18 +1,25 @@
 import "@material/mwc-linear-progress/mwc-linear-progress";
 import { mdiClose } from "@mdi/js";
-import { css, html, LitElement, nothing } from "lit";
+import { css, html, LitElement, nothing, type TemplateResult } from "lit";
 import { customElement, property, query, state } from "lit/decorators";
 import memoizeOne from "memoize-one";
 import { fireEvent } from "../../common/dom/fire_event";
+import "../../components/ha-alert";
 import "../../components/ha-dialog-header";
+import "../../components/ha-fade-in";
 import "../../components/ha-icon-button";
 import "../../components/ha-items-display-editor";
 import type { DisplayValue } from "../../components/ha-items-display-editor";
 import "../../components/ha-md-dialog";
 import type { HaMdDialog } from "../../components/ha-md-dialog";
 import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
+import "../../components/ha-spinner";
+import {
+  fetchFrontendUserData,
+  saveFrontendUserData,
+} from "../../data/frontend";
 import type { HomeAssistant } from "../../types";
-import type { EditSidebarDialogParams } from "./show-dialog-edit-sidebar";
+import { showConfirmationDialog } from "../generic/show-dialog-box";
 
 @customElement("dialog-edit-sidebar")
 class DialogEditSidebar extends LitElement {
@@ -22,21 +29,43 @@ class DialogEditSidebar extends LitElement {
 
   @query("ha-md-dialog") private _dialog?: HaMdDialog;
 
-  @state() private _order: string[] = [];
+  @state() private _order?: string[];
 
-  @state() private _hidden: string[] = [];
+  @state() private _hidden?: string[];
 
-  private _saveCallback?: (order: string[], hidden: string[]) => void;
+  @state() private _error?: string;
 
-  public async showDialog(params: EditSidebarDialogParams): Promise<void> {
+  /**
+   * If user has old localStorage values, show a confirmation dialog
+   */
+  @state() private _migrateToUserData = false;
+
+  public async showDialog(): Promise<void> {
     this._open = true;
 
-    const storedOrder = localStorage.getItem("sidebarPanelOrder");
-    const storedHidden = localStorage.getItem("sidebarHiddenPanels");
+    this._getData();
+  }
 
-    this._order = storedOrder ? JSON.parse(storedOrder) : this._order;
-    this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden;
-    this._saveCallback = params.saveCallback;
+  private async _getData() {
+    try {
+      const data = await fetchFrontendUserData(this.hass.connection, "sidebar");
+      this._order = data?.panelOrder;
+      this._hidden = data?.hiddenPanels;
+
+      // fallback to old localStorage values
+      if (!this._order) {
+        const storedOrder = localStorage.getItem("sidebarPanelOrder");
+        this._migrateToUserData = !!storedOrder;
+        this._order = storedOrder ? JSON.parse(storedOrder) : [];
+      }
+      if (!this._hidden) {
+        const storedHidden = localStorage.getItem("sidebarHiddenPanels");
+        this._migrateToUserData = this._migrateToUserData || !!storedHidden;
+        this._hidden = storedHidden ? JSON.parse(storedHidden) : [];
+      }
+    } catch (err: any) {
+      this._error = err.message || err;
+    }
   }
 
   private _dialogClosed(): void {
@@ -52,12 +81,16 @@ class DialogEditSidebar extends LitElement {
     panels ? Object.values(panels) : []
   );
 
-  protected render() {
-    if (!this._open) {
-      return nothing;
+  private _renderContent(): TemplateResult {
+    if (!this._order || !this._hidden) {
+      return html`<ha-fade-in .delay=${500}
+        ><ha-spinner size="large"></ha-spinner
+      ></ha-fade-in>`;
     }
 
-    const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar");
+    if (this._error) {
+      return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
+    }
 
     const panels = this._panels(this.hass.panels);
 
@@ -71,7 +104,7 @@ class DialogEditSidebar extends LitElement {
 
     const items = [
       ...beforeSpacer,
-      ...panels.filter((panel) => this._hidden.includes(panel.url_path)),
+      ...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
       ...afterSpacer.filter((panel) => panel.url_path !== "config"),
     ].map((panel) => ({
       value: panel.url_path,
@@ -89,6 +122,26 @@ class DialogEditSidebar extends LitElement {
       disableSorting: panel.url_path === "developer-tools",
     }));
 
+    return html`<ha-items-display-editor
+      .hass=${this.hass}
+      .value=${{
+        order: this._order,
+        hidden: this._hidden,
+      }}
+      .items=${items}
+      @value-changed=${this._changed}
+      dont-sort-visible
+    >
+    </ha-items-display-editor>`;
+  }
+
+  protected render() {
+    if (!this._open) {
+      return nothing;
+    }
+
+    const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar");
+
     return html`
       <ha-md-dialog open @closed=${this._dialogClosed}>
         <ha-dialog-header slot="headline">
@@ -98,26 +151,22 @@ class DialogEditSidebar extends LitElement {
             .path=${mdiClose}
             @click=${this.closeDialog}
           ></ha-icon-button>
-          <span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
+          <span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
+          ${!this._migrateToUserData
+            ? html`<span slot="subtitle"
+                >${this.hass.localize("ui.sidebar.edit_subtitle")}</span
+              >`
+            : nothing}
         </ha-dialog-header>
-        <div slot="content" class="content">
-          <ha-items-display-editor
-            .hass=${this.hass}
-            .value=${{
-              order: this._order,
-              hidden: this._hidden,
-            }}
-            .items=${items}
-            @value-changed=${this._changed}
-            dont-sort-visible
-          >
-          </ha-items-display-editor>
-        </div>
+        <div slot="content" class="content">${this._renderContent()}</div>
         <div slot="actions">
           <ha-button @click=${this.closeDialog}>
             ${this.hass.localize("ui.common.cancel")}
           </ha-button>
-          <ha-button @click=${this._save}>
+          <ha-button
+            .disabled=${!this._order || !this._hidden}
+            @click=${this._save}
+          >
             ${this.hass.localize("ui.common.save")}
           </ha-button>
         </div>
@@ -131,8 +180,27 @@ class DialogEditSidebar extends LitElement {
     this._hidden = [...hidden];
   }
 
-  private _save(): void {
-    this._saveCallback?.(this._order ?? [], this._hidden ?? []);
+  private async _save() {
+    if (this._migrateToUserData) {
+      const confirmation = await showConfirmationDialog(this, {
+        destructive: true,
+        text: this.hass.localize("ui.sidebar.migrate_to_user_data"),
+      });
+      if (!confirmation) {
+        return;
+      }
+    }
+
+    try {
+      await saveFrontendUserData(this.hass.connection, "sidebar", {
+        panelOrder: this._order!,
+        hiddenPanels: this._hidden!,
+      });
+    } catch (err: any) {
+      this._error = err.message || err;
+      return;
+    }
+
     this.closeDialog();
   }
 
@@ -149,6 +217,12 @@ class DialogEditSidebar extends LitElement {
         min-height: 100%;
       }
     }
+
+    ha-fade-in {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
   `;
 }
 
diff --git a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts
index 4a88bafd6ba9..dc8ce5b1ea90 100644
--- a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts
+++ b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts
@@ -1,18 +1,11 @@
 import { fireEvent } from "../../common/dom/fire_event";
 
-export interface EditSidebarDialogParams {
-  saveCallback: (order: string[], hidden: string[]) => void;
-}
-
 export const loadEditSidebarDialog = () => import("./dialog-edit-sidebar");
 
-export const showEditSidebarDialog = (
-  element: HTMLElement,
-  dialogParams: EditSidebarDialogParams
-): void => {
+export const showEditSidebarDialog = (element: HTMLElement): void => {
   fireEvent(element, "show-dialog", {
     dialogTag: "dialog-edit-sidebar",
     dialogImport: loadEditSidebarDialog,
-    dialogParams,
+    dialogParams: {},
   });
 };
diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts
index 75d5a54765d2..16656921c8fd 100644
--- a/src/layouts/home-assistant-main.ts
+++ b/src/layouts/home-assistant-main.ts
@@ -5,31 +5,23 @@ import type { HASSDomEvent } from "../common/dom/fire_event";
 import { fireEvent } from "../common/dom/fire_event";
 import { listenMediaQuery } from "../common/dom/media_query";
 import { toggleAttribute } from "../common/dom/toggle_attribute";
+import { computeRTLDirection } from "../common/util/compute_rtl";
 import "../components/ha-drawer";
 import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
 import type { HomeAssistant, Route } from "../types";
 import "./partial-panel-resolver";
-import { computeRTLDirection } from "../common/util/compute_rtl";
-import { storage } from "../common/decorators/storage";
 
 declare global {
   // for fire event
   interface HASSDomEvents {
     "hass-toggle-menu": undefined | { open?: boolean };
-    "hass-edit-sidebar": EditSideBarEvent;
     "hass-show-notifications": undefined;
   }
   interface HTMLElementEventMap {
-    "hass-edit-sidebar": HASSDomEvent<EditSideBarEvent>;
     "hass-toggle-menu": HASSDomEvent<HASSDomEvents["hass-toggle-menu"]>;
   }
 }
 
-interface EditSideBarEvent {
-  order: string[];
-  hidden: string[];
-}
-
 @customElement("home-assistant-main")
 export class HomeAssistantMain extends LitElement {
   @property({ attribute: false }) public hass!: HomeAssistant;
@@ -44,22 +36,6 @@ export class HomeAssistantMain extends LitElement {
 
   @state() private _drawerOpen = false;
 
-  @state()
-  @storage({
-    key: "sidebarPanelOrder",
-    state: true,
-    subscribe: true,
-  })
-  private _panelOrder: string[] = [];
-
-  @state()
-  @storage({
-    key: "sidebarHiddenPanels",
-    state: true,
-    subscribe: true,
-  })
-  private _hiddenPanels: string[] = [];
-
   constructor() {
     super();
     listenMediaQuery("(max-width: 870px)", (matches) => {
@@ -81,8 +57,6 @@ export class HomeAssistantMain extends LitElement {
           .hass=${this.hass}
           .narrow=${sidebarNarrow}
           .route=${this.route}
-          .panelOrder=${this._panelOrder}
-          .hiddenPanels=${this._hiddenPanels}
           .alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"}
         ></ha-sidebar>
         <partial-panel-resolver
@@ -106,14 +80,6 @@ export class HomeAssistantMain extends LitElement {
       );
     }
 
-    this.addEventListener(
-      "hass-edit-sidebar",
-      (ev: HASSDomEvent<EditSideBarEvent>) => {
-        this._panelOrder = ev.detail.order;
-        this._hiddenPanels = ev.detail.hidden;
-      }
-    );
-
     this.addEventListener("hass-toggle-menu", (ev) => {
       if (this._sidebarEditMode) {
         return;
diff --git a/src/panels/profile/ha-profile-section-general.ts b/src/panels/profile/ha-profile-section-general.ts
index a14043f6250d..46cbc1c268ac 100644
--- a/src/panels/profile/ha-profile-section-general.ts
+++ b/src/panels/profile/ha-profile-section-general.ts
@@ -150,6 +150,23 @@ class HaProfileSectionGeneral extends LitElement {
               .narrow=${this.narrow}
               .hass=${this.hass}
             ></ha-pick-first-weekday-row>
+            <ha-settings-row .narrow=${this.narrow}>
+              <span slot="heading">
+                ${this.hass.localize(
+                  "ui.panel.profile.customize_sidebar.header"
+                )}
+              </span>
+              <span slot="description">
+                ${this.hass.localize(
+                  "ui.panel.profile.customize_sidebar.description"
+                )}
+              </span>
+              <mwc-button @click=${this._customizeSidebar}>
+                ${this.hass.localize(
+                  "ui.panel.profile.customize_sidebar.button"
+                )}
+              </mwc-button>
+            </ha-settings-row>
             ${this.hass.user!.is_admin
               ? html`
                   <ha-advanced-mode-row
@@ -187,23 +204,6 @@ class HaProfileSectionGeneral extends LitElement {
               .narrow=${this.narrow}
               .hass=${this.hass}
             ></ha-pick-dashboard-row>
-            <ha-settings-row .narrow=${this.narrow}>
-              <span slot="heading">
-                ${this.hass.localize(
-                  "ui.panel.profile.customize_sidebar.header"
-                )}
-              </span>
-              <span slot="description">
-                ${this.hass.localize(
-                  "ui.panel.profile.customize_sidebar.description"
-                )}
-              </span>
-              <mwc-button @click=${this._customizeSidebar}>
-                ${this.hass.localize(
-                  "ui.panel.profile.customize_sidebar.button"
-                )}
-              </mwc-button>
-            </ha-settings-row>
             ${this.hass.dockedSidebar !== "auto" || !this.narrow
               ? html`
                   <ha-force-narrow-row
@@ -248,18 +248,9 @@ class HaProfileSectionGeneral extends LitElement {
   }
 
   private _customizeSidebar() {
-    showEditSidebarDialog(this, {
-      saveCallback: this._saveSidebar,
-    });
+    showEditSidebarDialog(this);
   }
 
-  private _saveSidebar = (order: string[], hidden: string[]) => {
-    fireEvent(this, "hass-edit-sidebar", {
-      order,
-      hidden,
-    });
-  };
-
   private _handleLogOut() {
     showConfirmationDialog(this, {
       title: this.hass.localize("ui.panel.profile.logout_title"),
diff --git a/src/translations/en.json b/src/translations/en.json
index 725074c0cac8..ff6bd50f7125 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -2019,7 +2019,9 @@
     "sidebar": {
       "external_app_configuration": "App settings",
       "sidebar_toggle": "Sidebar toggle",
-      "edit_sidebar": "Edit sidebar"
+      "edit_sidebar": "Edit sidebar",
+      "edit_subtitle": "Synced on all devices",
+      "migrate_to_user_data": "This will change the sidebar on all the devices you are logged into. To create a sidebar per device, you should use a different user for that device."
     },
     "panel": {
       "my": {


Reviews:
user: login: MindFreeze
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T14:51:15Z
user: login: bramkragten
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T08:49:30Z
user: login: bramkragten
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T08:52:13Z
user: login: wendevlin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-26T08:14:29Z
user: login: bramkragten
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-26T10:40:15Z
user: login: bramkragten
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-26T13:16:23Z

Review comments:
diff_hunk: @@ -22,21 +29,43 @@ class DialogEditSidebar extends LitElement {
 
   @query("ha-md-dialog") private _dialog?: HaMdDialog;
 
-  @state() private _order: string[] = [];
+  @state() private _order?: string[];
 
-  @state() private _hidden: string[] = [];
+  @state() private _hidden?: string[];
 
-  private _saveCallback?: (order: string[], hidden: string[]) => void;
+  @state() private _error?: string;
 
-  public async showDialog(params: EditSidebarDialogParams): Promise<void> {
+  /**
+   * If user has old localStorage values, show a confirmation dialog
+   */
+  private _migrateToUserData = false;
+
+  public async showDialog(): Promise<void> {
     this._open = true;
 
-    const storedOrder = localStorage.getItem("sidebarPanelOrder");
-    const storedHidden = localStorage.getItem("sidebarHiddenPanels");
+    this._getData();
+  }
 
-    this._order = storedOrder ? JSON.parse(storedOrder) : this._order;
-    this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden;
-    this._saveCallback = params.saveCallback;
+  private async _getData() {
+    try {
+      const data = await fetchFrontendUserData(this.hass.connection, "sidebar");
+      this._order = data?.panelOrder;
+      this._hidden = data?.hiddenPanels;
+
+      // fallback to old localStorage values
+      if (!this._order) {
+        const storedOrder = localStorage.getItem("sidebarPanelOrder");
+        this._migrateToUserData = !!storedOrder;
+        this._order = storedOrder ? JSON.parse(storedOrder) : [];
+      }
+      if (!this._hidden) {
+        const storedHidden = localStorage.getItem("sidebarHiddenPanels");
+        this._migrateToUserData = !!storedHidden;
path: src/dialogs/sidebar/dialog-edit-sidebar.ts
user: login: MindFreeze
body: ```suggestion
        this._migrateToUserData = this._migrateToUserData || !!storedHidden;
```
Not sure it is a real case but this change would handle when 'sidebarPanelOrder' is not empty and 'sidebarHiddenPanels' is empty
created_at: 2025-05-22T14:49:12Z
author_association: MEMBER
diff_hunk: @@ -2021,7 +2021,9 @@
     "sidebar": {
       "external_app_configuration": "App settings",
       "sidebar_toggle": "Sidebar toggle",
-      "edit_sidebar": "Edit sidebar"
+      "edit_sidebar": "Edit sidebar",
+      "edit_subtitle": "Synced on all devices",
path: src/translations/en.json
user: login: bramkragten
body: Not sure this is needed, as it also is not correct if the data is not in user data yet?
created_at: 2025-05-23T08:49:30Z
author_association: MEMBER
diff_hunk: @@ -2021,7 +2021,9 @@
     "sidebar": {
       "external_app_configuration": "App settings",
       "sidebar_toggle": "Sidebar toggle",
-      "edit_sidebar": "Edit sidebar"
+      "edit_sidebar": "Edit sidebar",
+      "edit_subtitle": "Synced on all devices",
+      "migrate_to_user_data": "This will change the sidebar on all of your devices. To create a sidebar per device, please create a separate user profile."
path: src/translations/en.json
user: login: bramkragten
body: ```suggestion
      "migrate_to_user_data": "This will change the sidebar on all the devices you are logged into. To create a sidebar per device, you should use a different user for that device."
```
created_at: 2025-05-23T08:52:13Z
author_association: MEMBER
diff_hunk: @@ -2021,7 +2021,9 @@
     "sidebar": {
       "external_app_configuration": "App settings",
       "sidebar_toggle": "Sidebar toggle",
-      "edit_sidebar": "Edit sidebar"
+      "edit_sidebar": "Edit sidebar",
+      "edit_subtitle": "Synced on all devices",
path: src/translations/en.json
user: login: wendevlin
body: As long as I don't hit save it's not user data but this dialog does sync it when hit save. But we can also just remove it, I am open to it. I just thought this is a nice way to inform the user it will change on all devices. Because the conformiation dialog will come just once.
created_at: 2025-05-26T08:14:29Z
author_association: MEMBER
diff_hunk: @@ -2021,7 +2021,9 @@
     "sidebar": {
       "external_app_configuration": "App settings",
       "sidebar_toggle": "Sidebar toggle",
-      "edit_sidebar": "Edit sidebar"
+      "edit_sidebar": "Edit sidebar",
+      "edit_subtitle": "Synced on all devices",
path: src/translations/en.json
user: login: bramkragten
body: We can also only show it when it is in the user data?
created_at: 2025-05-26T10:40:15Z
author_association: MEMBER

PR comments:
user: login: silamon
created_at: 2025-05-26T14:56:45Z
body: This probably needs a few extra tweaks to make the demo environment work. Also, the loading spinner is only displayed partly when the sidebar is in narrow mode. 
author_association: CONTRIBUTOR
user: login: wendevlin
created_at: 2025-05-26T15:23:58Z
body: > Also, the loading spinner is only displayed partly when the sidebar is in narrow mode.

Mhh I cannot reproduce this:
![image](https://github.com/user-attachments/assets/c311d6be-f1dc-49b3-b2de-8938d399581d)

author_association: MEMBER
user: login: silamon
created_at: 2025-05-26T15:27:24Z
body: It's actually the docked sidebar feature: 
![image](https://github.com/user-attachments/assets/3ddf3ded-753a-4ab1-a247-5544e66e0fb3)

author_association: CONTRIBUTOR
user: login: silamon
created_at: 2025-05-26T15:29:31Z
body: I think it will also break cast deployments when casting from a home assistant version that doesn't support this websocket command. 
author_association: CONTRIBUTOR
user: login: bramkragten
created_at: 2025-05-26T18:51:03Z
body: cast should not use the sidebar right? And this API has been around for a long time already...
author_association: MEMBER
user: login: wendevlin
created_at: 2025-05-26T19:12:50Z
body: > cast should not use the sidebar right? And this API has been around for a long time already...

Not the subscription api, its new.
author_association: MEMBER
user: login: wendevlin
created_at: 2025-05-27T07:59:45Z
body: @silamon 
- https://github.com/home-assistant/frontend/pull/25606
author_association: MEMBER