diff --git a/packages/component-base/src/style-props.js b/packages/component-base/src/style-props.js
index 77eb760915c..d445e685d71 100644
--- a/packages/component-base/src/style-props.js
+++ b/packages/component-base/src/style-props.js
@@ -61,12 +61,14 @@ addGlobalThemeStyles(
--_vaadin-icon-chevron-down: url('data:image/svg+xml,');
--_vaadin-icon-clock: url('data:image/svg+xml,');
--_vaadin-icon-cross: url('data:image/svg+xml;utf8,');
+ --_vaadin-icon-drag: url('data:image/svg+xml;utf8,');
--_vaadin-icon-eye: url('data:image/svg+xml;utf8,');
--_vaadin-icon-eye-slash: url('data:image/svg+xml;utf8,');
--_vaadin-icon-fullscreen: url('data:image/svg+xml;utf8,');
--_vaadin-icon-menu: url('data:image/svg+xml;utf8,');
--_vaadin-icon-minus: url('data:image/svg+xml;utf8,');
--_vaadin-icon-plus: url('data:image/svg+xml;utf8,');
+ --_vaadin-icon-resize: url('data:image/svg+xml;utf8,');
--_vaadin-icon-sort: url('data:image/svg+xml;utf8,');
--_vaadin-icon-user: url('data:image/svg+xml;utf8,');
--_vaadin-icon-warn: url('data:image/svg+xml;utf8,');
diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json
index 6ec5bbc238f..43cc83dd2db 100644
--- a/packages/dashboard/package.json
+++ b/packages/dashboard/package.json
@@ -21,6 +21,8 @@
"type": "module",
"files": [
"src",
+ "!src/styles/*-base-styles.d.ts",
+ "!src/styles/*-base-styles.js",
"theme",
"vaadin-*.d.ts",
"vaadin-*.js",
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-base-styles.d.ts b/packages/dashboard/src/styles/vaadin-dashboard-base-styles.d.ts
new file mode 100644
index 00000000000..2840d6000eb
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-base-styles.d.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import type { CSSResult } from 'lit';
+
+export const dashboardStyles: CSSResult;
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-base-styles.js b/packages/dashboard/src/styles/vaadin-dashboard-base-styles.js
new file mode 100644
index 00000000000..b047d1db944
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-base-styles.js
@@ -0,0 +1,25 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import { css } from 'lit';
+import { dashboardLayoutStyles } from './vaadin-dashboard-layout-core-styles.js';
+
+const dashboard = css`
+ #grid[item-resizing] {
+ -webkit-user-select: none;
+ user-select: none;
+ }
+
+ ::slotted(vaadin-dashboard-widget-wrapper) {
+ display: contents;
+ }
+`;
+
+export const dashboardStyles = [dashboardLayoutStyles, dashboard];
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-button-base-styles.d.ts b/packages/dashboard/src/styles/vaadin-dashboard-button-base-styles.d.ts
new file mode 100644
index 00000000000..09c43aec145
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-button-base-styles.d.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import type { CSSResult } from 'lit';
+
+export const dashboardButtonStyles: CSSResult;
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-button-base-styles.js b/packages/dashboard/src/styles/vaadin-dashboard-button-base-styles.js
new file mode 100644
index 00000000000..3567684324c
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-button-base-styles.js
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import { css } from 'lit';
+import { buttonStyles } from '@vaadin/button/src/styles/vaadin-button-core-styles.js';
+
+const dashboardButton = css`
+ :host {
+ min-width: 1rem;
+ }
+`;
+
+export const dashboardButtonStyles = [buttonStyles, dashboardButton];
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-layout-base-styles.d.ts b/packages/dashboard/src/styles/vaadin-dashboard-layout-base-styles.d.ts
new file mode 100644
index 00000000000..8ac5f349140
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-layout-base-styles.d.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import type { CSSResult } from 'lit';
+
+export const dashboardLayoutStyles: CSSResult;
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-layout-base-styles.js b/packages/dashboard/src/styles/vaadin-dashboard-layout-base-styles.js
new file mode 100644
index 00000000000..246a8c25bc6
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-layout-base-styles.js
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import { css } from 'lit';
+
+export const dashboardLayoutStyles = css`
+ :host {
+ display: block;
+ overflow: auto;
+ box-sizing: border-box;
+ width: 100%;
+ }
+
+ :host([hidden]) {
+ display: none !important;
+ }
+
+ :host([dense-layout]) #grid {
+ grid-auto-flow: dense;
+ }
+
+ #grid {
+ box-sizing: border-box;
+
+ /* Padding around dashboard edges */
+ --_default-padding: 1rem;
+ --_padding: max(0px, var(--vaadin-dashboard-padding, var(--_default-padding)));
+ padding: var(--_padding);
+
+ /* Gap between widgets */
+ --_default-gap: 1rem;
+ --_gap: max(0px, var(--vaadin-dashboard-gap, var(--_default-gap)));
+ gap: var(--_gap);
+
+ /* Default min and max column widths */
+ --_default-col-min-width: 25rem;
+ --_default-col-max-width: 1fr;
+
+ /* Effective min and max column widths */
+ --_col-min-width: var(--vaadin-dashboard-col-min-width, var(--_default-col-min-width));
+ --_col-max-width: var(--vaadin-dashboard-col-max-width, var(--_default-col-max-width));
+
+ /* Effective max column count */
+ --_col-max-count: var(--vaadin-dashboard-col-max-count, var(--_col-count));
+
+ /* Effective column count */
+ --_effective-col-count: min(var(--_col-count), var(--_col-max-count));
+
+ /* Default row min height */
+ --_default-row-min-height: 12rem;
+ /* Effective row min height */
+ --_row-min-height: var(--vaadin-dashboard-row-min-height, var(--_default-row-min-height));
+ /* Effective row height */
+ --_row-height: minmax(var(--_row-min-height, auto), auto);
+
+ display: grid;
+ overflow: hidden;
+ min-width: calc(var(--_col-min-width) + var(--_padding) * 2);
+
+ grid-template-columns: repeat(
+ var(--_effective-col-count, auto-fill),
+ minmax(var(--_col-min-width), var(--_col-max-width))
+ );
+
+ grid-auto-rows: var(--_row-height);
+ }
+
+ ::slotted(*) {
+ /* The grid-column value applied to children */
+ --_item-column: span min(var(--vaadin-dashboard-widget-colspan, 1), var(--_effective-col-count, var(--_col-count)));
+
+ grid-column: var(--_item-column);
+
+ /* The grid-row value applied to children */
+ --_item-row: span var(--vaadin-dashboard-widget-rowspan, 1);
+ grid-row: var(--_item-row);
+ }
+`;
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-section-base-styles.d.ts b/packages/dashboard/src/styles/vaadin-dashboard-section-base-styles.d.ts
new file mode 100644
index 00000000000..f2800bc6418
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-section-base-styles.d.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import type { CSSResult } from 'lit';
+
+export const dashboardSectionStyles: CSSResult;
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-section-base-styles.js b/packages/dashboard/src/styles/vaadin-dashboard-section-base-styles.js
new file mode 100644
index 00000000000..2eed48e11c4
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-section-base-styles.js
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import '@vaadin/component-base/src/style-props.js';
+import { css } from 'lit';
+import { dashboardWidgetAndSectionStyles } from './vaadin-dashboard-widget-section-core-styles.js';
+
+const sectionStyles = css`
+ :host {
+ display: grid;
+ position: relative;
+ grid-template-columns: subgrid;
+ --_section-column: 1 / calc(var(--_effective-col-count) + 1);
+ grid-column: var(--_section-column) !important;
+ gap: var(--_gap, 1rem);
+ /* Dashboard section header height */
+ --_section-header-height: minmax(0, auto);
+ grid-template-rows: var(--_section-header-height) repeat(auto-fill, var(--_row-height));
+ grid-auto-rows: var(--_row-height);
+ border-radius: var(--vaadin-radius-m);
+ --_section-outline-offset: calc(min(var(--_gap), var(--_padding)) / 3);
+ --_focus-ring-offset: calc((var(--_section-outline-offset) - var(--_focus-ring-width)));
+ }
+
+ :host([hidden]) {
+ display: none !important;
+ }
+
+ ::slotted(*) {
+ --_item-column: span min(var(--vaadin-dashboard-widget-colspan, 1), var(--_effective-col-count, var(--_col-count)));
+
+ grid-column: var(--_item-column);
+ --_item-row: span var(--vaadin-dashboard-widget-rowspan, 1);
+ grid-row: var(--_item-row);
+ }
+
+ header {
+ grid-column: var(--_section-column);
+ }
+
+ :host::before {
+ z-index: 2 !important;
+ }
+
+ ::slotted(vaadin-dashboard-widget-wrapper) {
+ display: contents;
+ }
+
+ /* Section states */
+
+ :host([editable]) {
+ outline: 1px solid var(--vaadin-border-color);
+ outline-offset: calc(var(--_section-outline-offset) - 1px);
+ }
+
+ :host([focused])::after {
+ content: '';
+ display: block;
+ position: absolute;
+ inset: 0;
+ border-radius: var(--vaadin-radius-m);
+ z-index: 9;
+ outline: var(--_focus-ring-width) solid var(--_focus-ring-color);
+ outline-offset: var(--_focus-ring-offset);
+ }
+`;
+
+export const dashboardSectionStyles = [sectionStyles, dashboardWidgetAndSectionStyles];
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-widget-base-styles.d.ts b/packages/dashboard/src/styles/vaadin-dashboard-widget-base-styles.d.ts
new file mode 100644
index 00000000000..db2adeaa9a7
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-widget-base-styles.d.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import type { CSSResult } from 'lit';
+
+export const dashboardWidgetStyles: CSSResult;
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-widget-base-styles.js b/packages/dashboard/src/styles/vaadin-dashboard-widget-base-styles.js
new file mode 100644
index 00000000000..9e2c7965465
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-widget-base-styles.js
@@ -0,0 +1,108 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import '@vaadin/component-base/src/style-props.js';
+import { css } from 'lit';
+import { dashboardWidgetAndSectionStyles } from './vaadin-dashboard-widget-section-core-styles.js';
+
+const widgetStyles = css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ grid-column: var(--_item-column);
+ grid-row: var(--_item-row);
+ position: relative;
+ background: var(--_widget-background);
+ border-radius: var(--_widget-border-radius);
+ box-shadow: var(--_widget-shadow);
+ }
+
+ :host::before {
+ content: '';
+ position: absolute;
+ inset: calc(-1 * var(--_widget-border-width));
+ border: var(--_widget-border-width) solid var(--_widget-border-color);
+ border-radius: calc(var(--_widget-border-radius) + var(--_widget-border-width));
+ pointer-events: none;
+ }
+
+ :host([hidden]) {
+ display: none !important;
+ }
+
+ :host(:not([editable])) [part~='resize-button'] {
+ display: none;
+ }
+
+ [part~='content'] {
+ flex: 1;
+ overflow: hidden;
+ min-height: 1rem;
+ }
+
+ [part~='resize-button'] {
+ position: absolute;
+ bottom: 0;
+ inset-inline-end: 0;
+ z-index: 1;
+ overflow: hidden;
+ cursor: nwse-resize;
+ --icon: var(--_vaadin-icon-resize);
+ }
+
+ :host([dir='rtl']) [part~='resize-button'] {
+ cursor: sw-resize;
+ }
+
+ :host([dir='rtl']) [part~='resize-button'] .icon::before {
+ transform: scaleX(-1);
+ }
+
+ /* Widget states */
+
+ :host([editable]) {
+ --vaadin-dashboard-widget-shadow: var(--_widget-editable-shadow);
+ --_widget-border-width: 1px;
+ }
+
+ :host([focused])::before {
+ border-width: var(--_focus-ring-width);
+ border-color: var(--_focus-ring-color);
+ }
+
+ :host([selected]) {
+ --vaadin-dashboard-widget-shadow: var(--_widget-selected-shadow);
+ }
+
+ :host([dragging]) {
+ box-shadow: none;
+ background: var(--_drop-target-background-color);
+ border: var(--_drop-target-border);
+ }
+
+ :host([resizing])::after {
+ content: '';
+ z-index: 2;
+ position: absolute;
+ top: -1px;
+ width: var(--_widget-resizer-width, 0);
+ height: var(--_widget-resizer-height, 0);
+ border-radius: inherit;
+ background: var(--_drop-target-background-color);
+ border: var(--_drop-target-border);
+ }
+
+ /* Widget parts */
+ header {
+ padding: var(--vaadin-padding-container);
+ }
+`;
+
+export const dashboardWidgetStyles = [widgetStyles, dashboardWidgetAndSectionStyles];
diff --git a/packages/dashboard/src/styles/vaadin-dashboard-widget-section-base-styles.js b/packages/dashboard/src/styles/vaadin-dashboard-widget-section-base-styles.js
new file mode 100644
index 00000000000..f8a1845ce1a
--- /dev/null
+++ b/packages/dashboard/src/styles/vaadin-dashboard-widget-section-base-styles.js
@@ -0,0 +1,278 @@
+/**
+ * @license
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ *
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
+ * license.
+ */
+import '@vaadin/component-base/src/style-props.js';
+import { css } from 'lit';
+
+export const dashboardWidgetAndSectionStyles = css`
+ :host {
+ box-sizing: border-box;
+ --_widget-background: var(--vaadin-dashboard-widget-background, var(--vaadin-background-color));
+ --_widget-border-radius: var(--vaadin-dashboard-widget-border-radius, var(--vaadin-radius-m));
+ --_widget-border-width: var(--vaadin-dashboard-widget-border-width, 1px);
+ --_widget-border-color: var(--vaadin-dashboard-widget-border-color, var(--vaadin-border-color));
+ --_widget-shadow: var(--vaadin-dashboard-widget-shadow, 0 0 0 0 transparent);
+ --_widget-editable-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);
+ --_widget-selected-shadow: 0 3px 12px -1px rgba(0, 0, 0, 0.3);
+ --_drop-target-background-color: var(--vaadin-dashboard-drop-target-background-color, rgba(0, 0, 0, 0.1));
+ --_focus-ring-color: var(--vaadin-focus-ring-color);
+ --_focus-ring-width: var(--vaadin-focus-ring-width);
+ }
+
+ :host([focused]) {
+ z-index: 1;
+ }
+
+ :host([dragging]) * {
+ visibility: hidden;
+ }
+
+ :host(:not([editable])) [part~='move-button'],
+ :host(:not([editable])) [part~='remove-button'],
+ :host(:not([editable])) #focus-button,
+ :host(:not([editable])) #focus-button-wrapper,
+ :host(:not([editable])) .mode-controls {
+ display: none;
+ }
+
+ #focustrap {
+ display: contents;
+ }
+
+ header {
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ justify-content: space-between;
+ overflow: hidden;
+ }
+
+ [part='title'] {
+ flex: 1;
+ color: var(--vaadin-color);
+ white-space: var(--vaadin-dashboard-widget-title-wrap, wrap);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin: 0 0 1px;
+ align-self: safe center;
+ }
+
+ vaadin-dashboard-button {
+ z-index: 1;
+ padding: 4px;
+ }
+
+ vaadin-dashboard-button .icon::before {
+ display: block;
+ content: '';
+ height: var(--vaadin-icon-size, 24px);
+ width: var(--vaadin-icon-size, 24px);
+ background: currentColor;
+ mask-image: var(--icon);
+ }
+
+ #focus-button-wrapper,
+ #focus-button {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ }
+
+ #focus-button {
+ pointer-events: none;
+ padding: 0;
+ border: none;
+ }
+
+ .mode-controls {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ }
+
+ .mode-controls[hidden] {
+ display: none;
+ }
+
+ /* Drag handle */
+ [part~='move-button'] {
+ cursor: move;
+ --icon: var(--_vaadin-icon-drag);
+ }
+
+ /* Remove button */
+ [part~='remove-button'] {
+ cursor: pointer;
+ margin-inline-start: 4px;
+ --icon: var(--_vaadin-icon-cross);
+ }
+
+ /* Move-mode buttons */
+ [part~='move-backward-button'],
+ [part~='move-forward-button'],
+ [part~='move-apply-button'] {
+ position: absolute;
+ top: 50%;
+ }
+
+ [part~='move-backward-button'] {
+ inset-inline-start: 0;
+ transform: translateY(-50%);
+ --icon: var(--_vaadin-icon-chevron-down);
+ }
+
+ [part~='move-forward-button'] {
+ inset-inline-end: 0;
+ transform: translateY(-50%);
+ --icon: var(--_vaadin-icon-chevron-down);
+ }
+
+ [part~='move-apply-button'] {
+ left: 50%;
+ transform: translate(-50%, -50%);
+ --icon: var(--_vaadin-icon-checkmark);
+ }
+
+ :host([first-child]) [part~='move-backward-button'],
+ :host([last-child]) [part~='move-forward-button'] {
+ display: none;
+ }
+
+ :host(:not([dir='rtl'])) [part~='move-backward-button'],
+ :host([dir='rtl']) [part~='move-forward-button'] {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ :host(:not([dir='rtl'])) [part~='move-forward-button'],
+ :host([dir='rtl']) [part~='move-backward-button'] {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ :host(:not([dir='rtl'])) [part~='move-backward-button'] .icon,
+ :host([dir='rtl']) [part~='move-forward-button'] .icon {
+ rotate: 90deg;
+ }
+
+ :host(:not([dir='rtl'])) [part~='move-forward-button'] .icon,
+ :host([dir='rtl']) [part~='move-backward-button'] .icon {
+ rotate: -90deg;
+ }
+
+ /* Resize-mode buttons */
+ [part~='resize-shrink-width-button'],
+ [part~='resize-shrink-height-button'],
+ [part~='resize-grow-width-button'],
+ [part~='resize-grow-height-button'],
+ [part~='resize-apply-button'] {
+ position: absolute;
+ }
+
+ [part~='resize-shrink-width-button'] {
+ inset-inline-end: 0;
+ top: 50%;
+ }
+
+ :host(:not([dir='rtl'])) [part~='resize-shrink-width-button'] {
+ transform: translateY(-50%) translateX(-100%);
+ }
+
+ :host([dir='rtl']) [part~='resize-shrink-width-button'] {
+ transform: translateY(-50%) translateX(100%);
+ }
+
+ .mode-controls:has([part~='resize-grow-width-button'][hidden]) [part~='resize-shrink-width-button'] {
+ transform: translateY(-50%);
+ }
+
+ [part~='resize-grow-width-button'] {
+ inset-inline-start: 100%;
+ top: 50%;
+ }
+
+ :host(:not([dir='rtl'])) [part~='resize-grow-width-button'] {
+ transform: translateY(-50%) translateX(-100%);
+ }
+
+ :host([dir='rtl']) [part~='resize-grow-width-button'] {
+ transform: translateY(-50%) translateX(100%);
+ }
+
+ [part~='resize-shrink-height-button'] {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%) translateY(-100%);
+ }
+
+ [part~='resize-grow-height-button'] {
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-100%);
+ }
+
+ [part~='resize-grow-height-button'],
+ [part~='resize-grow-width-button'] {
+ --icon: var(--_vaadin-icon-plus);
+ }
+
+ [part~='resize-shrink-height-button'],
+ [part~='resize-shrink-width-button'] {
+ --icon: var(--_vaadin-icon-minus);
+ }
+
+ [part~='resize-apply-button'] {
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ --icon: var(--_vaadin-icon-checkmark);
+ }
+
+ [part~='resize-shrink-width-button'] + [part~='resize-grow-width-button'] {
+ margin-left: 1px;
+ }
+
+ [part~='resize-grow-height-button'],
+ [part~='resize-shrink-height-button'] {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ [part~='resize-shrink-height-button']:not([hidden]) + [part~='resize-grow-height-button'] {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ [part~='resize-shrink-height-button'] + [part~='resize-grow-height-button'] {
+ margin-top: 1px;
+ }
+
+ :host(:not([dir='rtl'])) [part~='resize-grow-width-button'],
+ :host(:not([dir='rtl'])) [part~='resize-shrink-width-button'] {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ :host([dir='rtl']) [part~='resize-grow-width-button'],
+ :host([dir='rtl']) [part~='resize-shrink-width-button'] {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ :host(:not([dir='rtl'])) [part~='resize-shrink-width-button']:not([hidden]) + [part~='resize-grow-width-button'] {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ :host([dir='rtl']) [part~='resize-shrink-width-button']:not([hidden]) + [part~='resize-grow-width-button'] {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+`;
diff --git a/packages/dashboard/test/visual/base/dashboard.test.ts b/packages/dashboard/test/visual/base/dashboard.test.ts
new file mode 100644
index 00000000000..f97a2b9bb97
--- /dev/null
+++ b/packages/dashboard/test/visual/base/dashboard.test.ts
@@ -0,0 +1,176 @@
+import { sendKeys } from '@vaadin/test-runner-commands';
+import { fixtureSync, nextFrame } from '@vaadin/testing-helpers';
+import { visualDiff } from '@web/test-runner-visual-regression';
+import '../../../src/vaadin-dashboard.js';
+import { html, render } from 'lit';
+import type { Dashboard } from '../../../src/vaadin-dashboard.js';
+import {
+ describeBidirectional,
+ fireDragStart,
+ fireResizeOver,
+ fireResizeStart,
+ getDraggable,
+ getElementFromCell,
+ getResizeHandle,
+} from '../../helpers.js';
+
+describe('dashboard', () => {
+ let focusElement: HTMLInputElement;
+ let element: Dashboard;
+ let div: HTMLDivElement;
+
+ beforeEach(() => {
+ div = document.createElement('div');
+
+ fixtureSync(`
+
+ `);
+
+ focusElement = fixtureSync(``);
+ focusElement.focus();
+ });
+
+ describeBidirectional(`widgets and section`, () => {
+ function getName(name: string) {
+ return `${document.dir || 'ltr'}-${name}`;
+ }
+
+ beforeEach(async () => {
+ element = fixtureSync(``, div);
+
+ element.renderer = (wrapper) => {
+ render(
+ html`
+ Header content
+ Content
+ `,
+ wrapper,
+ );
+ };
+
+ element.items = [
+ { title: 'Widget 1', colspan: 1 },
+ { title: 'Widget 2', colspan: 1 },
+ {
+ title: 'Section title',
+ items: [{ colspan: 1, rowspan: 1 }, { colspan: 1 }],
+ },
+ ];
+ await nextFrame();
+ });
+
+ it('default', async () => {
+ await visualDiff(div, getName('default'));
+ });
+
+ it('focused widget', async () => {
+ element.editable = true;
+ await sendKeys({ press: 'Tab' });
+ await visualDiff(div, getName('focused-widget'));
+ });
+
+ it('selected widget', async () => {
+ element.editable = true;
+ await sendKeys({ press: 'Tab' });
+ await sendKeys({ press: 'Enter' });
+ await visualDiff(div, getName('selected-widget'));
+ });
+
+ it('resize mode', async () => {
+ element.editable = true;
+ const firstWidget = getElementFromCell(element, 0, 0)!;
+ const resizeHandle = getResizeHandle(firstWidget) as HTMLElement;
+ resizeHandle.click();
+ await nextFrame();
+ await visualDiff(div, getName('resize-mode'));
+ });
+
+ it('move mode', async () => {
+ element.editable = true;
+ const firstWidget = getElementFromCell(element, 0, 0)!;
+ const moveHandle = getDraggable(firstWidget) as HTMLElement;
+ moveHandle.click();
+ await nextFrame();
+ await visualDiff(div, getName('move-mode'));
+ });
+
+ it('dragged widget', async () => {
+ element.editable = true;
+ fireDragStart(getElementFromCell(element, 0, 0)!);
+ await nextFrame();
+
+ await visualDiff(div, getName('dragged-widget'));
+ });
+
+ it('resized widget', async () => {
+ element.editable = true;
+ fireResizeStart(getElementFromCell(element, 0, 0)!);
+ await nextFrame();
+ fireResizeOver(getElementFromCell(element, 0, 0)!, 'end');
+ await nextFrame();
+
+ await visualDiff(div, getName('resized-widget'));
+ });
+
+ it('no gap', async () => {
+ element.style.setProperty('--vaadin-dashboard-gap', '0px');
+ await nextFrame();
+ await visualDiff(div, getName('no-gap'));
+ });
+
+ it('editable', async () => {
+ element.editable = true;
+ await visualDiff(div, getName('editable'));
+ });
+
+ describe('long title', () => {
+ beforeEach(async () => {
+ element.items = [
+ { colspan: 1 },
+ { colspan: 1 },
+ {
+ title:
+ 'Section long title: Nunc sit amet suscipit tellus, id fermentum massa. Aliquam vel tellus cursus, sodales ligula sed, iaculis justo.',
+ items: [{ colspan: 1, rowspan: 1 }, { colspan: 1 }],
+ },
+ ];
+
+ element.renderer = (wrapper) => {
+ render(
+ html`
+ Header content
+ Content
+ `,
+ wrapper,
+ );
+ };
+
+ await nextFrame();
+ });
+
+ it('title wrap', async () => {
+ await nextFrame();
+ await visualDiff(div, getName('title-wrap'));
+ });
+
+ it('no title wrap', async () => {
+ element.style.setProperty('--vaadin-dashboard-widget-title-wrap', 'nowrap');
+ await nextFrame();
+ await visualDiff(div, getName('no-title-wrap'));
+ });
+ });
+ });
+});
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-default.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-default.png
new file mode 100644
index 00000000000..1df691213a7
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-default.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-dragged-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-dragged-widget.png
new file mode 100644
index 00000000000..31042085dc6
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-dragged-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-editable.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-editable.png
new file mode 100644
index 00000000000..267b970eb1b
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-editable.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-focused-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-focused-widget.png
new file mode 100644
index 00000000000..8bc2c2047a9
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-focused-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-move-mode.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-move-mode.png
new file mode 100644
index 00000000000..4a481e2f0b8
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-move-mode.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-no-gap.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-no-gap.png
new file mode 100644
index 00000000000..38c949e04cc
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-no-gap.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-no-title-wrap.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-no-title-wrap.png
new file mode 100644
index 00000000000..a65c798efcb
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-no-title-wrap.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-resize-mode.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-resize-mode.png
new file mode 100644
index 00000000000..689030d3e61
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-resize-mode.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-resized-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-resized-widget.png
new file mode 100644
index 00000000000..24f856b5d04
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-resized-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-selected-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-selected-widget.png
new file mode 100644
index 00000000000..e760eb0c9d5
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-selected-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-title-wrap.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-title-wrap.png
new file mode 100644
index 00000000000..ef8a713f785
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/ltr-title-wrap.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-default.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-default.png
new file mode 100644
index 00000000000..2d1f3d77027
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-default.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-dragged-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-dragged-widget.png
new file mode 100644
index 00000000000..a1dc4325b92
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-dragged-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-editable.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-editable.png
new file mode 100644
index 00000000000..55b1e95c9b7
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-editable.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-focused-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-focused-widget.png
new file mode 100644
index 00000000000..78c81aae4f7
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-focused-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-move-mode.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-move-mode.png
new file mode 100644
index 00000000000..12d1cb41c37
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-move-mode.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-no-gap.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-no-gap.png
new file mode 100644
index 00000000000..863524ffe81
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-no-gap.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-no-title-wrap.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-no-title-wrap.png
new file mode 100644
index 00000000000..cc6e3416f79
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-no-title-wrap.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-resize-mode.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-resize-mode.png
new file mode 100644
index 00000000000..0bfe60db455
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-resize-mode.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-resized-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-resized-widget.png
new file mode 100644
index 00000000000..c4d6764bc73
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-resized-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-selected-widget.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-selected-widget.png
new file mode 100644
index 00000000000..150b0e64cd9
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-selected-widget.png differ
diff --git a/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-title-wrap.png b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-title-wrap.png
new file mode 100644
index 00000000000..6276f3d6335
Binary files /dev/null and b/packages/dashboard/test/visual/base/screenshots/dashboard/baseline/rtl-title-wrap.png differ