diff --git a/PRODUCT.md b/PRODUCT.md
index 5083f0f27..8b4a6c01a 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -10,7 +10,7 @@ Homarr is used by self-hosters and administrators who assemble dashboards for se
## Product Purpose
-Homarr provides one place to organize, observe, and operate self-hosted services. Success means a user can build a useful dashboard quickly, understand its state, recover from configuration errors, and extend it without requiring a bespoke Homarr feature for every service.
+Homarr provides one place to organize, observe, and operate self-hosted services. Success means a user can build a useful dashboard quickly, understand its state, recover from configuration errors, and extend it without requiring a bespoke Homarr feature for every service. Boards must load immediately, stay visually stable, keep widgets legible across screen sizes, and make editing feel direct and forgiving.
## Brand Personality
@@ -27,7 +27,9 @@ Avoid decorative admin dashboards, wizard flows that fight ordinary scrolling, h
3. Make simple tasks short without limiting advanced use cases.
4. Isolate failures so one service or widget cannot disrupt the dashboard.
5. Use the existing Mantine design language consistently across management and board surfaces.
+6. Keep spatial actions direct: moving, resizing, nesting, and using fixed rails happen on the canvas with immediate feedback.
+7. Preserve predictable widget composition with fixed logical tile sizes and horizontal scrolling before controls become inaccessible.
## Accessibility & Inclusion
-Target WCAG 2.2 AA. Support keyboard navigation, screen readers, visible focus, sufficient contrast, reduced motion, non-color status cues, responsive layouts, and actionable error messages.
+Target WCAG 2.2 AA. Support keyboard navigation, screen readers, visible focus, sufficient contrast, reduced motion, non-color status cues, responsive layouts, actionable error messages, and keyboard equivalents for drag and resize operations.
diff --git a/apps/docs/docs/getting-started/after-the-installation.mdx b/apps/docs/docs/getting-started/after-the-installation.mdx
index 7ac904bf4..f5f389590 100644
--- a/apps/docs/docs/getting-started/after-the-installation.mdx
+++ b/apps/docs/docs/getting-started/after-the-installation.mdx
@@ -54,11 +54,7 @@ You must choose a username and password (both can later be changed).
We enforce strict password requirements when you enter your password to protect your instance if you accidentally
expose it to the internet.
-
+
If you selected external providers (such as LDAP or OIDC), you'll be prompted to create an admin group.
This group will be used to synchronize users from your provider to Homarr.
@@ -116,22 +112,14 @@ The onboarding can no longer be started again.
From here you can click on one of the provided links to continue your journey.
We recommend you to continue with the section [creating your first board on this page](#creating-your-first-board).
-
+
### Importing a ZIP from version before 1.0.0
This is recommended when you used Homarr before and want to re-import your data.
It is important that you first migrate correctly using [the list of breaking changes for 1.0](/blog/2024/09/23/version-1.0).
-
+
**How to export and import data**
@@ -149,7 +137,8 @@ It is important that you first migrate correctly using [the list of breaking cha
## Creating your first board
In Homarr, you organize your apps and widgets on boards.
-Boards can be organized using the interactive drag and drop.
+Boards use fixed-size tiles so the same widget keeps the same proportions on every screen.
+Drag, resize, and section controls are available in edit mode.
Let's create your first board by navigating here: `Manage` > `Boards` > `New board`:

@@ -197,38 +186,23 @@ Here you have the following options in the header:
When entering the edit mode, you'll get some additional buttons to work with:
-
+
- **Add item**: Click this button to add new elements to your page.
-- **Exit edit mode**: Click this btuton to save your changes and exit edit mode.
+- **Exit edit mode**: Click this button to save your changes and exit edit mode.
To continue, click the option "New Item":
-
+
Next, you can choose what item you'd like to add.
We will discuss the other options in the following sections - for now choose "App" to continue.
-
+
As soon as you chose the app, the modal will close and you'll get the following item on your board:
-
+
This is expected, because you haven't told Homarr what app it should show in this item.
To configure this, click on the small 3 dots at the top of the item in the edit mode and choose "Edit item" in the menu.
@@ -240,21 +214,27 @@ In the modal you can edit the app that you want to display:

As soon as you save your changes, the tile on the board will refresh and display your app.
-Using the small bidirectional arrow at the bottom right you can resize the element.
-Start pressing with your mouse button down, move your mouse to resize and release your finger as soon your you're happy with the size.
+Use the resize handle at the bottom right to resize the tile.
+Press and hold the handle, move the pointer, and release it when the tile has the size you want.

-To move your item to a different position on the board, simply hold the mouse button anywhere on the item and move with the mouse to the desired position.
-As soon as you release your finger, the item will snap in place in the grid.
+To move a tile in edit mode, press and hold anywhere on the tile except one of its buttons or controls, move it to the desired position, and release it.
+The tile snaps to the grid and neighboring tiles move out of the way automatically.

+Every `1 × 1` tile has a logical content size of `200 × 200` pixels.
+Larger tiles use multiples of that fixed unit, with a consistent gap between cells.
+Homarr scales the complete board uniformly, like zooming a page.
+It stops shrinking at 75% and lets you scroll horizontally instead of making text and controls too small.
+It does not squeeze or reflow the content of individual widgets, so a widget keeps the same design and proportions across screen sizes.
+
:::note
-You cannot move items freely on the vertical axis.
-We always align all items to the top of the page to simply organization.
-To move items to the bottom, you must add more items or resize items.
+Viewing a board initially uses the lightweight, non-interactive layout.
+For users with edit access, Homarr prepares drag-and-drop and resize controls in the background after the board becomes interactive.
+If you enter edit mode before that finishes, the existing board stays visible until the editor is ready.
:::
@@ -265,8 +245,10 @@ This helps you to explore the capabilities of Homarr before you build your board
:::
-Users on touch or mobile devices may not be able to drag and drop or move items.
-They can use the "Move / resize item" option in the edit menu of items instead:
+Keyboard users can focus a tile, press Enter or Space, then use arrow keys to move and Shift plus arrow keys to resize.
+Press Escape to stop.
+For precise numeric controls or touch input, choose **Move / resize item** from the tile's edit menu.
+The dialog can also move the tile to another section or fixed sidebar:

@@ -338,43 +320,39 @@ As with every change, don't forget to save your changes using the exit edit mode

-## Grouping your items
+## Organizing items with sections
-As the last step, let's organize your freshly created items using two different options.
-You can choose either or both of them on your boards.
+Sections are resizable containers for apps, widgets, and other sections.
+The same section works on the main canvas and inside a fixed sidebar.
+Each section can configure:
-### Using dynamic sections
-
-Dynamic sections are a simple, resizable visual group to group items.
+- Show or hide its label.
+- Allow or disable collapsing.
+- Show or hide **Open all in new tabs**.
+- Change its border color and custom CSS classes.

-After adding a dynamic section, you can simply drag and drop items into the section:
+After adding a section, drag and resize it like any other tile, then drag items into it:

-### Using categories
+### Labeled and collapsible sections
-Categories are more advanced and complicated than dynamic sections.
-To create a category, open the add item modal and choose "new category":
+Open the add-content menu and choose **New section**.
+Use its settings menu to add a label and enable the behavior you want.
+In edit mode, move items into or out of the section directly.
+In view mode, each user can collapse eligible sections without changing the saved board layout.
+If **Open all in new tabs** is enabled, the section menu can open every app link it contains.
-
+### Fixed sidebars
-Every category must have a name. After you click "Add category", you will get a new item on your board:
+Open the board's **Layout** settings to enable the left or right sidebar for each responsive layout.
+Choose one, two, or three columns with the slider and use the preview to check the remaining canvas width.
+The sidebar reserves those columns from the dashboard, so Homarr moves canvas items into the next available positions.
+Sidebar content stays visible while the page scrolls and remains editable.
-
-
-You can drag and drop items in to this category in the edit mode.
-Categories will have a big space before and after them in the edit mode to
-enable you to drag and drop items outside the category.
-You can also collapse or open categories in edit mode to facilitate your editing process.
-
-:::note
-
-A current limitation of categories is, that they will always have a width of 100%.
-You cannot reduce their horizontal width.
-
-:::
+Existing categories become sections automatically, and category rails become fixed sidebars.
## Guided tours
diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx
index d13fd6674..145e221ea 100644
--- a/apps/docs/docs/management/api/index.mdx
+++ b/apps/docs/docs/management/api/index.mdx
@@ -84,6 +84,13 @@ The OpenAPI and MCP APIs include board management endpoints for common automatio
Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags.
+### Saving dashboard layouts
+
+The dashboard editor saves item positions and sections as one board update.
+Canvas height follows its content automatically and does not require a separate API setting.
+Responsive layouts store optional left and right gutter widths from zero to three columns.
+Gutter columns are reserved from the layout's total column count, and changing them reflows affected items.
+
## Get Custom Widget authoring resources
Custom JSX v2 provides four HTTP discovery endpoints:
diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx
index 93bf78ae3..f10ead270 100644
--- a/apps/docs/docs/management/boards/index.mdx
+++ b/apps/docs/docs/management/boards/index.mdx
@@ -36,8 +36,10 @@ Underscores and hyphen-minus are allowed.
### Column count
-Homarr has a complex drag-and-drop system that can be used from the browser.
-The column count defines how many horizontal columns are available to drag and drop elements to.
+The column count defines how many fixed-width cells are available in each row.
+One cell contains a `200 × 200` logical-pixel tile.
+Homarr scales the whole board uniformly instead of changing widget proportions, and always fits it to the available width without horizontal scrolling.
+The main canvas and enabled rails stay at least one viewport tall, and the canvas grows automatically while you drag or resize items near its lower edge.
### Public
@@ -88,26 +90,78 @@ Homarr will fall back to the Homarr logo if nothing is set.
### Layout
-In this section you can configure the different responsive layouts. Depending on your screens width the layout will change.
+In this section you can configure the different responsive layouts.
+Homarr selects a layout from the screen width, then scales its complete fixed-size canvas uniformly.

-On each element the position and size of items can be changed seperately.
-When adding a new layout the position and sizes will automatically be generated depending on the column count.
-When adding a new item / dynamic section that will always be 1x1 on each layout and placed on the first available position.
+Every `1 × 1` tile has a `200 × 200` logical-pixel content area.
+The `24` logical-pixel gap between tiles is separate from that size.
+A `2 × 1` tile therefore contains two fixed cell widths plus the gap between them.
+Widgets see their logical size, while the board's visual scale makes the complete layout larger or smaller.
+When fitting below 100%, Homarr keeps widget typography and control density at their normal readable size.
+On larger canvases, the complete board grows together like browser zoom.
+The complete canvas always fits the available width and never adds horizontal board scrolling.
+
+The position and size of every item are independent in each responsive layout.
+Moving or resizing an item in one layout does not change any other layout.
+When adding a new layout, its initial positions and sizes are generated from the column count.
+New items use their widget's default grid size, while a new Container starts at a useful `4 × 3` size when the layout has enough columns.
+Both are placed in the first available position.
+
+The canvas is at least one window tall, even when it is empty.
+Its left and right edges are hard drag boundaries, while its bottom edge extends automatically as a tile is dragged or resized downward.
+Nested Containers keep their own resizable bounds.
#### Name
-The name of the layout can be choosen freely. It is only used for the management and will not be visible on the board.
+The name of the layout can be chosen freely. It is only used for management and will not be visible on the board.
#### Column count
-Amount of columns for this layout. Defines how many items can be placed in a row. When changed it will automatically adjust the position and size of the items.
+Total number of columns for this layout.
+When changed, Homarr moves items into the next available positions without overlap.
+
+#### Fixed rails
+
+Each responsive layout can enable a fixed left rail, a fixed right rail, or both.
+Set a rail width to zero to disable it for that layout.
+Choose a width from one to three columns with the three-step slider.
+The preview shows how much space remains for the main canvas.
+
+Rail columns are reserved from the layout's total column count.
+For example, a 12-column layout with a two-column left rail leaves ten columns for the canvas.
+Enabling or widening a rail reflows canvas items into the remaining columns.
+Disabling a rail moves its contents back into the next available canvas positions.
+
+Rails fill the available window height and remain fixed while the page scrolls. They do not grow or scroll independently.
+Apps, widgets, and Containers inside them still use the same fixed `200 × 200` cells and can be rearranged in edit mode, where each rail shows its name and boundary.
#### Breakpoint
Minimum width of the screen where the layout will be used.
-Once the minimum of the next layout is reached the layout will change.
+Once the minimum of the next layout is reached, the layout will change.
+
+#### View and edit modes
+
+View mode renders a static board without executing the drag-and-drop editor.
+After the board is interactive, Homarr prepares the editor in the background for users who can modify it.
+Entering edit mode keeps the static board visible until that preparation finishes, then enables collision-aware movement and resizing without changing the board geometry.
+Widget content is inert in edit mode, so dragging anywhere on the content moves the tile instead of activating the widget; its settings button remains visible in the compact header above the tile.
+Dropping a tile into another tile's exact slot swaps their positions when both resulting placements fit without overlapping other content.
+For other collisions, or when a tile grows into occupied space, nearby tiles are compacted out of the way.
+The visible corner controls provide a 44-pixel resize target, and edge controls appear on hover for horizontal or vertical resizing.
+Resize handles stop at the tile's minimum allowed size while the pointer is still moving.
+Exit edit mode to save the resulting positions.
+
+Pointer users can drag anywhere on a widget tile. Containers use the grab button at their top-left corner.
+Resize tiles from any edge or corner handle.
+On a touch screen, press and hold the tile before moving it; resize handles support direct touch input.
+Keyboard users can focus a tile, press Enter or Space to start editing, use the arrow keys to move it, and use Shift plus the arrow keys to resize it.
+Press Enter, Space, or Escape to stop keyboard editing.
+Keyboard moves and resizes apply immediately; Escape stops keyboard editing without undoing those changes.
+During a pointer or touch interaction, Escape cancels the move or resize and restores the layout from before that interaction.
+The **Move / resize item** action in the edit menu remains available for precise numeric input.
#### Preview
@@ -115,6 +169,24 @@ Once the minimum of the next layout is reached the layout will change.
+### Containers and rails
+
+Containers can hold apps, widgets, and nested Containers.
+The same Container model works on the main canvas, inside a fixed rail, or inside another Container.
+Move and resize Containers directly in edit mode; nested content stays within the Container's local bounds.
+Resize outlines follow the pointer continuously while a dashed preview marks the grid size that will be applied on release.
+Each responsive layout stores its own Container position and size.
+
+Each Container can configure:
+
+- Its label and whether that label is visible.
+- Whether users can collapse and expand it like an accordion.
+- Whether its menu shows **Open all in new tabs** for the app links it contains.
+- Its border color and custom CSS classes.
+
+Collapsing a Container reduces its full-width header to a half-row expansion bar and compacts the surrounding layout without losing its expanded size.
+Expanding it smoothly restores those bounds and moves following items out of the way without reloading the widgets inside it.
+
### Background
#### Background image URL
@@ -159,7 +231,7 @@ Change the color of your bookmark / app icons.
#### Item radius
-Change the radius of your items / categories.
+Change the radius of your items and Containers.
diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts
index 2105259be..240a3f47e 100644
--- a/apps/nextjs/next.config.ts
+++ b/apps/nextjs/next.config.ts
@@ -79,6 +79,10 @@ const nextConfig: NextConfig = {
{
source: "/(.*)", // Apply CSP to all routes
headers: [
+ {
+ key: "Accept-CH",
+ value: "Sec-CH-Viewport-Width",
+ },
{
key: "Content-Security-Policy",
// worker-src / media-src with blob: is necessary for video.js, see https://github.com/homarr-labs/homarr/issues/3912 and https://stackoverflow.com/questions/65792855/problem-with-video-js-and-content-security-policy-csp
diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json
index 157a012a7..2c269d59c 100644
--- a/apps/nextjs/package.json
+++ b/apps/nextjs/package.json
@@ -32,7 +32,6 @@
"@homarr/docker": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/forms-collection": "workspace:^0.1.0",
- "@homarr/gridstack": "catalog:",
"@homarr/icons": "workspace:^0.1.0",
"@homarr/image-proxy": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
@@ -83,6 +82,7 @@
"drizzle-orm": "catalog:",
"flag-icons": "catalog:",
"glob": "catalog:",
+ "gridstack": "catalog:",
"isomorphic-dompurify": "catalog:",
"jotai": "catalog:",
"jszip": "catalog:",
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css
new file mode 100644
index 000000000..fe7d183d2
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css
@@ -0,0 +1,20 @@
+.columns {
+ display: grid;
+ align-items: start;
+ gap: 24px;
+}
+
+.lane {
+ min-width: 0;
+}
+
+.gutter {
+ position: sticky;
+ top: 0;
+ align-self: start;
+ max-height: calc(100dvh - var(--app-shell-header-offset, 0px) - var(--mantine-spacing-md));
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ scrollbar-gutter: stable;
+ transform: translateY(var(--board-gutter-sticky-offset, 0px));
+}
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
index 1b4308d38..19bf1d0fa 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
@@ -1,33 +1,121 @@
"use client";
-import { Box, Stack } from "@mantine/core";
+import { Box } from "@mantine/core";
-import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
+import { useCurrentLayout, useInitialViewportWidth, useRequiredBoard } from "@homarr/boards/context";
+import { useEditMode } from "@homarr/boards/edit-mode";
+import { useScopedI18n } from "@homarr/translation/client";
-import { BoardCategorySection } from "~/components/board/sections/category-section";
+import {
+ getBoardLaneColumnCount,
+ getInitialBoardLogicalHeight,
+ getLogicalTrackSize,
+ LOGICAL_GRID_GAP,
+ getRootSectionForLane,
+} from "~/components/board/layout";
+import { ScaledBoardCanvas } from "~/components/board/layout/scaled-board-canvas";
import { BoardEmptySection } from "~/components/board/sections/empty-section";
+import { BoardGridEditorBoundary } from "~/components/board/sections/grid/board-grid-editor-boundary";
+import { BoardGridPortalHost, BoardGridPortalRenderer } from "~/components/board/sections/grid/grid-portal-host";
+import { BoardSectionCollapseProvider } from "~/components/board/sections/section-collapse";
import { BoardBackgroundVideo } from "~/components/layout/background";
+import classes from "./_client.module.css";
+
+const APP_SHELL_INLINE_PADDING = 32;
export const ClientBoard = () => {
const board = useRequiredBoard();
+ const t = useScopedI18n("board.landmark");
+ const [isEditMode] = useEditMode();
const currentLayoutId = useCurrentLayout();
+ const initialViewportWidth = useInitialViewportWidth();
+ const currentLayout = board.layouts.find((layout) => layout.id === currentLayoutId) ?? board.layouts.at(0);
+ if (!currentLayout) throw new Error("Expected the board to contain a layout");
- const fullWidthSortedSections = board.sections
- .filter((section) => section.kind === "empty" || section.kind === "category")
- .toSorted((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
+ const mainSection = getRootSectionForLane(board, "main");
+ const leftSection = getRootSectionForLane(board, "left");
+ const rightSection = getRootSectionForLane(board, "right");
+ if (!mainSection) throw new Error("Expected the board to contain a main canvas section");
+
+ const leftColumnCount = getBoardLaneColumnCount(currentLayout, "left");
+ const mainColumnCount = getBoardLaneColumnCount(currentLayout, "main");
+ const rightColumnCount = getBoardLaneColumnCount(currentLayout, "right");
+ const laneWidths = [leftColumnCount, mainColumnCount, rightColumnCount]
+ .filter((columnCount) => columnCount > 0)
+ .map(getLogicalTrackSize);
+ const logicalWidth =
+ laneWidths.reduce((total, width) => total + width, 0) + (laneWidths.length - 1) * LOGICAL_GRID_GAP;
+ const initialLogicalHeight = getInitialBoardLogicalHeight(board, currentLayoutId) + LOGICAL_GRID_GAP;
+ const initialAvailableWidth = Math.max(1, initialViewportWidth - APP_SHELL_INLINE_PADDING);
+ const gridTemplateColumns = [
+ leftColumnCount > 0 ? `${getLogicalTrackSize(leftColumnCount)}px` : null,
+ `${getLogicalTrackSize(mainColumnCount)}px`,
+ rightColumnCount > 0 ? `${getLogicalTrackSize(rightColumnCount)}px` : null,
+ ]
+ .filter((value) => value !== null)
+ .join(" ");
return (
-
- {fullWidthSortedSections.map((section) =>
- section.kind === "empty" ? (
-
- ) : (
-
- ),
- )}
-
+
+
+
+
+
+