Allow columns in Device Info - Settings to be resized or scale to screen width

It seems that when in the Settings>Devices & Services> Devices Tab> Pick a drive - the Device Info that are shown in columns do not size well to the page. When there are sliders for example the actual entity name is completely hidden from view making it hard to see what each parameter is.

I feel it should either scale better width wise, or handle the text better by wrapping it down so you can see all of it. Auto handling it would be best, but at least the option to adjust column width in settings here would be a start.

Additionally if the entity name is very long, when you click the entity to open up the popup for that entity the name can also be truncated vs wrapping the name so you can see it completely.

Please find the attached pictures for reference.

I will be linking two other posts in the forums to here to hopefully gain traction on this.

Thanks!

I second this but I think that solution should be increasing the width of the middle column rather than all three of them uniformly as that would just waste space too.

Cheers!

I’d like the columns drag-resizeable please. If that’s not possible then have the entity-ID column wider at least as the full text rarely fits in

+1

Not sure why the decision was made to force the container to a static width of 1000px.

Using the code snippet below should remove the max-width property from the container if pasted into the dev console. Now to figure out how to inject it reliably whenever I visit a device page.

document.querySelector('home-assistant')
    .shadowRoot.querySelector('home-assistant-main')
    .shadowRoot.querySelector('ha-config-device-page')
    .shadowRoot.querySelector('hass-subpage')
        .querySelector('.container').style.maxWidth = 'none';

Yep, the current state of this is nigh-unusable for some devices as you have to hover and wait for the tooltip just to see what anything is.

Does not look consistent to some other parts of HA - but since you asked…

customElements.whenDefined('ha-config-device-page').then(() => {
  const DevicePage = customElements.get('ha-config-device-page');
  const { html, css } = DevicePage.prototype;
  
  const newStyle = css`
  hass-subpage > .container {
    max-width: none;}`;
  
  const newStyles = [].concat(DevicePage.styles, newStyle);
  Object.defineProperty(DevicePage, 'styles',        {value: newStyles, configurable: true, enumerable: false});
  Object.defineProperty(DevicePage, 'elementStyles', {value: newStyles, configurable: true, enumerable: false});
});
frontend:
  extra_module_url:
    - /local/js-extenstions/test/device_page_wide.js

Works. Fantastic.
Should be implemented in main code.
Thank you.

Thank you!! I just implemented this. This is such a QOL improvement on looking at my entities. It should definitely be in default config.

I tried changing none to 90% or 80%, but see the file isn’t being reloaded. Does it only reload on reboot?

It is reloaded after purging a browser cache.

Thank you SOOO much for this!

@Ildar_Gabdullin - I’m trying to do something similar for the configuration column within the page but failing to get the correct syntax. Any chance you can help:

If I select the <label class="mdc-text-field mdc-text-field--no-label mdc-text-field--filled ">, the Styles tab shows

.mdc-text-field {
    width: 100%;
}

If I change this from width to instead set a min-width, it fixes the UI:

But my attempts to put it into device_page_wide.js aren’t successful at fixing the UI:

  const newStyle = css`
  hass-subpage > .container {
    max-width: 90%;}
  hass-subpage > .mdc-text-field {
    min-width: 120px;}
    `;

Any help is appreciated!

Sorry, cannot say anything helpful, very far from pc.

can you list the steps on how to do this please , dealing with a pretty annoying bug here

+1 to make the web UI more flexible and not static. Is there already anm issue for that on github? I guess here would nothing happen from the dev side!?

I could not find an open issue and/or discussion, so I created one here: https://github.com/orgs/home-assistant/discussions/2039

I don’t think that just removing the max-width is the proper solution. I suppose that the ā€œgridā€ should be adapted so that each column gets at least 500px. When not enough width is available, the columns should ā€œstackā€.

It looks great, just the way I wanted it.
But what do I do with this code? I’m completely lost!
Could you please explain it in a little more detail?

  1. Create a new file, place it in ā€œwwwā€ folder. You may create a subfolder if you like.
  2. Name this file ā€œdevice_page_wide.jsā€.
  3. Paste the posted JS code into this file.
  4. Open ā€œconfiguration.yamlā€, check for a presence of these lines:
frontend:
  extra_module_url:
  1. If these lines are absent - add them along with the ā€œ- /local/device_page_wide.jsā€ line:
frontend:
  extra_module_url:
    - /local/device_page_wide.js
  1. If these lines are present - just add that ā€œ- /local/device_page_wide.jsā€ to the list.
  2. Reboot HA.
  3. Purge a browser cache.

I would want a general redisign of this page in the future, since the columns are often very unbalanced.

I have quite a wide screen and would be happy about more then 3 columns.

Stuff like device info and Activities could be in its own container at the top, rest could somehow float to fill the space.

A slight variation to the aforementioned device_page_wide.js that keeps the left device info column rather small and extends the second (controls etc.) and third (activity), with more emphasis on the second column:

customElements.whenDefined('ha-config-device-page').then(() => {
    const DevicePage = customElements.get('ha-config-device-page');
    const { html, css } = DevicePage.prototype;

    const newStyle = css`
  hass-subpage > .container {
        max-width: none;
    }
  hass-subpage > .container > div:nth-child(1 of .column) {
        max-width: 400px;
    }
  hass-subpage > .container > div:nth-child(3 of .column) {
        flex-grow: 0.6;
    }`;

    const newStyles = [].concat(DevicePage.styles, newStyle);
    Object.defineProperty(DevicePage, 'styles', { value: newStyles, configurable: true, enumerable: false });
    Object.defineProperty(DevicePage, 'elementStyles', { value: newStyles, configurable: true, enumerable: false });
});

Ratios can be freely adjusted.

Generally a redesign would indeed be welcome, or the ability to arrange cards freely via drag and drop and have the arrangement stored (for example to move activity under device info and have sensors, controls and diagnostic as columns next to each other).

Here is a more complete alternative that allows dragging and resizing columns.

Put a file named adaptive-ha-device-page.js into /config/www/
Then put the entry into the configuration.yaml
Restart home assistant
Reload browser cache with ctrl+shift+r

frontend:
  extra_module_url:
    - /local/adaptive-ha-device-page.js

adaptive-ha-device-page.js

/* ================================================================
 * Adaptive wrapper for Home Assistant device configuration page
 * ----------------------------------------------------------------
 * - Removes the 1000px width cap so the page uses your full monitor.
 * - Per-column maxWidth / flexGrow / visibility.
 * - Drag bars between columns (persist to localStorage; dbl-click resets).
 * - Optional sub-column split inside the entity column.
 *
 * COLUMN LAYOUT (this is what HA renders, left to right):
 *   Column 1: Device Info  +  Related (Automations / Scenes / Scripts)
 *   Column 2: Entity cards (Controls, Sensors, Configs, Diagnostics)
 *   Column 3: Activity (Logbook)
 *
 * Device Info and Related share column 1 and stack vertically — that's
 * the HA template, not configurable from CSS.
 *
 * INSTALLATION
 *   1. Save as /config/www/adaptive-ha-device-page.js
 *   2. In configuration.yaml:
 *        frontend:
 *          extra_module_url:
 *            - /local/adaptive-ha-device-page.js
 *   3. Restart Home Assistant, then hard-reload (Ctrl+Shift+R).
 *
 * To reset a manually-resized column: double-click its drag bar.
 * To reset all columns: clear keys matching "adaptive-ha-device-page-*"
 * from localStorage and reload.
 * ================================================================ */

(() => {
  "use strict";

  // ============================ CONFIG ============================

  const CONFIG = {
    // Max width of the entire page area. 'none' = full screen width.
    containerMaxWidth: "none",

    // Per-column settings (1 = leftmost).
    //   maxWidth : hard upper bound on the column width
    //   flexGrow : relative share of leftover space
    //   visible  : false hides the column
    columns: {
      1: { maxWidth: "400px", flexGrow: 1, visible: true },
      2: { maxWidth: "none", flexGrow: 4, visible: true },
      3: { maxWidth: "450px", flexGrow: 1, visible: true },
    },

    // Drag bars between columns. Drag = resize, dbl-click = reset.
    // Resized widths persist via localStorage.
    resizable: true,

    // Split column 2 (entities) into multiple sub-columns. Uses CSS
    // multi-column so heights auto-balance for tight packing.
    //   entitiesSubColumns       : how many sub-columns to aim for
    //   entitiesSubColumnMinWidth: minimum width per sub-column; the
    //                              browser collapses to fewer columns
    //                              when there isn't room
    entitiesSubColumns: 2,
    entitiesSubColumnMinWidth: "320px",

    // Minimum width a column can be dragged down to (px).
    minColumnWidth: 200,
  };

  // ================================================================

  const STYLE_ID = "adaptive-wrapper-device-page-styles";
  const HANDLE_CLASS = "adaptive-resize-handle";
  const LS_KEY = (idx) => `adaptive-ha-device-page-col${idx}-width`;

  // ------------------------------ CSS ------------------------------
  // Note: we use `div:nth-child(N of .column)` (CSS Selectors L4)
  // rather than `.column:nth-of-type(N)`, because the container also
  // holds a non-column header div as its first child. nth-of-type
  // would count the header and target the wrong column.

  const buildCss = () => {
    const { containerMaxWidth, columns } = CONFIG;

    const columnRule = (n) => {
      const c = columns[n];
      if (!c) return "";
      const lines = [];
      if (c.visible === false) lines.push("display: none !important;");
      // Override original `width: 33%` so max-width is the real cap.
      lines.push("width: auto;");
      lines.push("flex-basis: 0;");
      lines.push(`flex-grow: ${c.flexGrow ?? 1};`);
      lines.push(`max-width: ${c.maxWidth ?? "none"};`);
      // Without min-width: 0, flex items refuse to shrink below their
      // content's min-content, which breaks flex-grow ratios.
      lines.push("min-width: 0;");
      return `
        hass-subpage > .container > div:nth-child(${n} of .column) {
          ${lines.join("\n          ")}
        }`;
    };

    // Sub-column split for the entity column. Uses CSS multi-column
    // (column-count) so heights auto-balance between sub-columns —
    // this gives tight packing without gaps under shorter cards.
    //
    // `contain: layout style` isolates the fragmentation so it can't
    // delay dropdown popup anchoring (the popup-below-icon bug from
    // earlier). A getBoundingClientRect flush after style injection
    // reinforces this — see applyAll() below.
    const subColumnsRule =
      CONFIG.entitiesSubColumns > 1
        ? `
        hass-subpage > .container > div:nth-child(2 of .column) {
          column-count: ${CONFIG.entitiesSubColumns};
          column-width: ${CONFIG.entitiesSubColumnMinWidth};
          column-gap: var(--ha-space-4, 16px);
          column-fill: balance;
          contain: layout style;
        }
        hass-subpage > .container > div:nth-child(2 of .column) > * {
          break-inside: avoid;
          -webkit-column-break-inside: avoid;
          page-break-inside: avoid;
          display: block;
          width: 100%;
          margin-top: 0 !important;
          margin-bottom: var(--ha-space-4, 16px);
        }`
        : "";

    const handleCss = CONFIG.resizable
      ? `
        .${HANDLE_CLASS} {
          flex: 0 0 8px;
          align-self: stretch;
          cursor: ew-resize;
          position: relative;
          background: transparent;
          z-index: 1;
          touch-action: none;
        }
        .${HANDLE_CLASS}::before {
          content: '';
          position: absolute;
          top: var(--ha-space-2, 8px);
          bottom: var(--ha-space-2, 8px);
          left: 50%;
          transform: translateX(-50%);
          width: 2px;
          background: var(--divider-color, rgba(127,127,127,0.35));
          border-radius: 1px;
          transition: background 120ms ease, width 120ms ease;
        }
        .${HANDLE_CLASS}:hover::before,
        .${HANDLE_CLASS}:active::before {
          background: var(--primary-color, #03a9f4);
          width: 4px;
        }
        :host([narrow]) .${HANDLE_CLASS} {
          display: none !important;
        }`
      : "";

    return `
      /* Page container */
      hass-subpage > .container {
        max-width: ${containerMaxWidth} !important;
      }

      /* Per-column rules */
      ${columnRule(1)}
      ${columnRule(2)}
      ${columnRule(3)}

      /* Multi-column entity grid */
      ${subColumnsRule}

      /* Drag handles */
      ${handleCss}

      /* Mobile: stack everything vertically */
      :host([narrow]) .container > .column {
        width: 100% !important;
        max-width: none !important;
        column-count: 1 !important;
        contain: none !important;
        flex-basis: auto !important;
      }
    `;
  };

  // ----------------------- Drag handle logic -----------------------

  const lockColumnWidth = (col, widthPx) => {
    col.style.flex = `0 0 ${widthPx}px`;
    col.style.maxWidth = "none";
    col.style.width = `${widthPx}px`;
  };

  const unlockColumnWidth = (col) => {
    [
      "flex",
      "flex-grow",
      "flex-shrink",
      "flex-basis",
      "width",
      "max-width",
    ].forEach((p) => col.style.removeProperty(p));
  };

  const setupResizeHandles = (devicePageEl) => {
    if (!CONFIG.resizable) return;
    const root = devicePageEl.shadowRoot;
    if (!root) return;
    const container = root.querySelector(".container");
    if (!container) return;

    const columns = Array.from(
      container.querySelectorAll(":scope > .column")
    );
    if (columns.length < 2) return;

    // Clean slate.
    container
      .querySelectorAll(`.${HANDLE_CLASS}`)
      .forEach((h) => h.remove());

    columns.forEach((col, idx) => {
      // Restore any saved width for this column.
      try {
        const saved = parseFloat(localStorage.getItem(LS_KEY(idx)) || "");
        if (!Number.isNaN(saved) && saved > 100) {
          lockColumnWidth(col, saved);
        }
      } catch {
        /* localStorage blocked — ignore */
      }

      // Insert a handle after every column except the last.
      if (idx === columns.length - 1) return;

      const handle = document.createElement("div");
      handle.className = HANDLE_CLASS;
      handle.title = "Drag to resize • Double-click to reset";
      col.insertAdjacentElement("afterend", handle);

      let startX = 0;
      let startWidth = 0;
      let dragging = false;

      const onMove = (e) => {
        if (!dragging) return;
        const delta = e.clientX - startX;
        const newWidth = Math.max(
          CONFIG.minColumnWidth,
          startWidth + delta
        );
        lockColumnWidth(col, newWidth);
      };
      const onUp = () => {
        if (!dragging) return;
        dragging = false;
        document.body.style.cursor = "";
        document.body.style.userSelect = "";
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onUp);
        try {
          localStorage.setItem(
            LS_KEY(idx),
            String(col.getBoundingClientRect().width)
          );
        } catch {
          /* ignore */
        }
      };

      handle.addEventListener("mousedown", (e) => {
        startX = e.clientX;
        startWidth = col.getBoundingClientRect().width;
        dragging = true;
        document.body.style.cursor = "ew-resize";
        document.body.style.userSelect = "none";
        document.addEventListener("mousemove", onMove);
        document.addEventListener("mouseup", onUp);
        e.preventDefault();
      });

      handle.addEventListener("dblclick", () => {
        unlockColumnWidth(col);
        try {
          localStorage.removeItem(LS_KEY(idx));
        } catch {
          /* ignore */
        }
      });
    });
  };

  // -------------------------- Style injection ---------------------

  const injectInto = (root, content) => {
    if (!root) return;
    let style = root.getElementById?.(STYLE_ID);
    if (style) {
      style.textContent = content;
      return;
    }
    style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = content;
    root.appendChild(style);
  };

  // Walk into shadow roots — needed for already-mounted instances.
  const findAll = (root, tag) => {
    const out = [];
    const lower = tag.toLowerCase();
    const walk = (node) => {
      if (!node) return;
      if (node.nodeType === 1 && node.tagName?.toLowerCase() === lower) {
        out.push(node);
      }
      if (node.shadowRoot) {
        for (const child of node.shadowRoot.children) walk(child);
      }
      for (const child of node.children || []) walk(child);
    };
    walk(root);
    return out;
  };

  const applyAll = (devicePageEl) => {
    injectInto(devicePageEl.shadowRoot, buildCss());
    // Run on next frame so the columns exist in the DOM.
    requestAnimationFrame(() => {
      setupResizeHandles(devicePageEl);
      // Force a synchronous layout pass so the multi-column
      // fragmentation has fully settled before the user can open
      // any dropdowns. Without this, getBoundingClientRect() in
      // HA's dropdown library can return a stale rect and anchor
      // the popup a few pixels below where the icon ends up.
      const c = devicePageEl.shadowRoot?.querySelector(".container");
      if (c) void c.getBoundingClientRect();
    });
  };

  // -------------------------- Patch the element -------------------

  customElements.whenDefined("ha-config-device-page").then(() => {
    const DevicePage = customElements.get("ha-config-device-page");
    if (!DevicePage) {
      console.warn("[adaptive-device-page] element not found");
      return;
    }

    const origFirstUpdated = DevicePage.prototype.firstUpdated;
    DevicePage.prototype.firstUpdated = function (...args) {
      const r = origFirstUpdated?.apply(this, args);
      applyAll(this);
      return r;
    };

    // If HA re-renders and our handles get blown away, put them back.
    const origUpdated = DevicePage.prototype.updated;
    DevicePage.prototype.updated = function (...args) {
      const r = origUpdated?.apply(this, args);
      if (CONFIG.resizable) {
        const c = this.shadowRoot?.querySelector(".container");
        if (c && !c.querySelector(`.${HANDLE_CLASS}`)) {
          requestAnimationFrame(() => setupResizeHandles(this));
        }
      }
      return r;
    };

    // Patch any instance already on the page when the script loads.
    findAll(document.body, "ha-config-device-page").forEach(applyAll);

    console.info("[adaptive-device-page] loaded");
  });
})();