diff --git a/docs/ux/datagrid.md b/docs/ux/datagrid.md
index 6baf997d34..9aab8180aa 100644
--- a/docs/ux/datagrid.md
+++ b/docs/ux/datagrid.md
@@ -40,9 +40,8 @@ More specifically, these are any combination of the following elements. Each ele
(TODO: Illustration of the fully featured header with overlayed/indicated zones)
-### Zone 1: Bulk Actions, Sorting, Action(s)
+### Zone 1: Sorting, Action(s)
-- Bulk Actions: Actions that can be run on multiple items in the DataGrid at once. Contains a Checkbox to select/deselect all items, and requires a Checkbox on each individual item / row.
- Sorting: Select to choose what to sort by, and a button to toggle sort direction
- Other Actions Overflow Menu: Any actions global to the data set other than the primary action
- Primary Action: The primary, i.e. most likely action for users. If users can create new items in the DataGrid, this can be done using the primary action button
@@ -53,10 +52,11 @@ More specifically, these are any combination of the following elements. Each ele
- Search: A SearchBox to perform a string-based search on the data and display the matching items. Make sure to manage user expectations as to what exactly will be searched, i.e. if the string entered is only matched against a single or a subset of fields, the placeholder should indicate that (e.g. "Search Description" if only descriptions can be searched)
- Filter Pills: Each filter that has been configured by the user using the Filter element(s) and that is currently active is displayed here. As a last item, there is a button that allows for clearing all active filters at once.
-### Zone 3: DataGrid View/State, Refresh
+### Zone 3: DataGrid View/State, Bulk Actions, Refresh
-- Item Count: The number of items of the set that is currently visible as a whole (not only on the current page). If filters or searches are active, the number of matching items and the total number of items in the data set is being displayed.
-- Last Update, Update/Refresh: The date and time of the last refresh of the data displayed, and a button to trigger a refresh.
+- Bulk Actions: Actions that can be run on multiple items in the DataGrid at once. Positioned on the left side of Zone 3. Contains a Checkbox to select/deselect all items, and a menu of available bulk actions. Requires a Checkbox on each individual item / row. Bulk action controls should be disabled until at least one item is selected. The select-all checkbox follows a tri-state model: unchecked when no items are selected, indeterminate when some are, and checked when all are. Clicking the checkbox in the unchecked or indeterminate state selects all items; clicking it in the checked state deselects all.
+- Item Count: The number of items of the set that is currently visible as a whole (not only on the current page), displayed in the center. If filters or searches are active, the number of matching items and the total number of items in the data set is being displayed.
+- Last Update, Update/Refresh: The date and time of the last refresh of the data displayed, and a button to trigger a refresh. Positioned on the right side of Zone 3.
All of the above elements are optional in the sense that none of them will be required for any given DataGrid. However, if you find yourself in a situation where none of the above options is needed or desired, reconsider whether using a DataGrid is the right choice to display the given data. This case can occur, but it is rare. In most cases a simple list is then a more appropriate option to display the data.
diff --git a/packages/ui-components/src/components/Button/Button.component.tsx b/packages/ui-components/src/components/Button/Button.component.tsx
index 03338bc55f..2ee6d9a8a8 100644
--- a/packages/ui-components/src/components/Button/Button.component.tsx
+++ b/packages/ui-components/src/components/Button/Button.component.tsx
@@ -22,7 +22,6 @@ const btnBase = `
jn:justify-center
jn:items-center
jn:rounded
- jn:shadow-sm
jn:w-auto
jn:focus:outline-hidden
jn:focus-visible:ring-2
diff --git a/packages/ui-components/src/components/DataGrid/DataGridHeader.stories.tsx b/packages/ui-components/src/components/DataGrid/DataGridHeader.stories.tsx
new file mode 100644
index 0000000000..f765aad21e
--- /dev/null
+++ b/packages/ui-components/src/components/DataGrid/DataGridHeader.stories.tsx
@@ -0,0 +1,383 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from "react"
+import type { Meta, StoryObj } from "@storybook/react-vite"
+import { DataGrid } from "./DataGrid.component"
+import { DataGridRow } from "../DataGridRow"
+import { DataGridCell } from "../DataGridCell"
+import { DataGridHeadCell } from "../DataGridHeadCell"
+import { DataGridCheckboxCell } from "../DataGridCheckboxCell"
+import { DataGridToolbar } from "../DataGridToolbar"
+import { Stack } from "../Stack"
+import { Button } from "../Button"
+import { Checkbox } from "../Checkbox"
+import { Select } from "../Select"
+import { SelectOption } from "../SelectOption"
+import { SortButton } from "../SortButton"
+import { PopupMenu, PopupMenuOptions, PopupMenuItem, PopupMenuToggle } from "../PopupMenu"
+import { InputGroup } from "../InputGroup"
+import { ComboBox } from "../ComboBox"
+import { ComboBoxOption } from "../ComboBoxOption"
+import { SearchInput } from "../SearchInput"
+import { Pill } from "../Pill"
+import { PortalProvider } from "../PortalProvider"
+
+const meta: Meta = {
+ title: "Components/DataGrid/DataGrid Header",
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ component: `
+The DataGrid header is a composition pattern, not a single component. It sits above the column headers and holds everything users need to interact with and configure the dataset.
+
+The header is structured in up to three zones:
+
+- **Zone 1 — Actions:** Sorting controls, an optional overflow menu for global actions, and the primary action (e.g. "Create"). This zone has no background — it is a plain \`Stack\` with no wrapper component.
+- **Zone 2 — Filters and Search:** One or more filter inputs (typically a \`Select\` + \`ComboBox\` in an \`InputGroup\`), a \`SearchInput\`, and active filter pills with a "Clear filters" button.
+- **Zone 3 — DataGrid State:** Bulk actions (select-all checkbox + action menu) on the left, item count in the middle (total, or "X of Y" when filters are active), and last update timestamp with a refresh button on the right.
+
+Zones 2 and 3 each use their own \`DataGridToolbar\`, which provides the background, padding, and separation from the grid.
+
+Every zone and every element within a zone is optional. Only include what the specific DataGrid needs. If none of the above is needed, reconsider whether a DataGrid is the right pattern at all.
+ `,
+ },
+ },
+ },
+}
+
+export default meta
+type Story = StoryObj
+
+const servers = [
+ { id: "1", name: "node-prod-01", region: "eu-west-1", status: "Running", az: "AZ-1" },
+ { id: "2", name: "node-prod-02", region: "eu-west-1", status: "Stopped", az: "AZ-2" },
+ { id: "3", name: "node-staging-01", region: "us-east-1", status: "Running", az: "AZ-1" },
+ { id: "4", name: "node-dev-01", region: "ap-south-1", status: "Error", az: "AZ-3" },
+]
+
+export const WithPrimaryAction: Story = {
+ render: () => (
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "The simplest DataGrid header: a single primary action. No `DataGridToolbar` needed — Zone 1 content sits directly in a `Stack`.",
+ },
+ source: { type: "dynamic" },
+ },
+ },
+}
+
+export const WithFiltersSearchAndState: Story = {
+ render: () => (
+ <>
+
+
+ {/* Zone 2: Filters + Search */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Zone 2 cont: Active filter pills */}
+
+
+
+
+
+
+
+
+ {/* Zone 3: Item count + Refresh */}
+
+ Showing 2 of 4 servers
+
+ Last update: 20.05.2026 @09:41
+
+
+
+
+ >
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Zones 2 and 3: filter controls, active filter pills, search, item count, and a refresh button. `DataGridToolbar` provides the background and spacing. Zone 1 is not needed here — no bulk actions or primary action required for this view.",
+ },
+ source: { type: "dynamic" },
+ },
+ },
+}
+
+export const WithSearchOnly: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Zone 2 with search only — no filter controls needed. Use this when the dataset doesn't require structured filtering.",
+ },
+ source: { type: "dynamic" },
+ },
+ },
+}
+
+export const FullyFeatured: Story = {
+ render: () => {
+ const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc")
+
+ return (
+ <>
+
+ {/* Zone 1: Sorting + primary action — bare Stack, no background */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Zones 2+3 — DataGridToolbar provides background and spacing */}
+
+
+ {/* Zone 2: Filters + Search */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Zone 2 cont: Active filter pills */}
+
+
+
+
+
+
+
+
+ {/* Zone 3: Bulk actions + item count + refresh */}
+
+
+
+
+
+
+
+
+
+
+
+ Showing 2 of 4 servers
+
+ Last update: 20.05.2026 @09:41
+
+
+
+
+
+
+
+
+
+ Name
+ Region
+ Status
+ Availability Zone
+
+
+ {servers.map((s) => (
+
+
+ {s.name}
+ {s.region}
+ {s.status}
+ {s.az}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ >
+ )
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Fully featured DataGrid header. Zone 1 (sort, overflow menu, primary action) is a bare `Stack` — no background, no `DataGridToolbar`. Zones 2 and 3 each get their own `DataGridToolbar`. Zone 3 carries bulk actions (checkbox + action menu) on the left, item count in the middle, and last update + refresh on the right. Every zone and every element within it is optional.",
+ },
+ // Keep this source.code in sync with the render function above
+ source: {
+ code: `
+
+ {/* Zone 1 — bare Stack, no background */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Zone 2 — DataGridToolbar provides background and spacing */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Zone 3 — separate DataGridToolbar */}
+
+
+
+
+
+
+
+
+
+
+
+
+ Showing 2 of 4 servers
+
+ Last update: 20.05.2026 @09:41
+
+
+
+
+
+
+
+
+
+ Name
+ Region
+ Status
+ Availability Zone
+
+
+ {servers.map((s) => (
+
+
+ {s.name}
+ {s.region}
+ {s.status}
+ {s.az}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ `.trim(),
+ },
+ },
+ },
+}
diff --git a/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx b/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx
index a545bda594..a2fef688d6 100644
--- a/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx
+++ b/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx
@@ -19,11 +19,12 @@ const cellBaseStyles = (nowrap: boolean, cellVerticalAlignment: CellVerticalAlig
`
: ""
}
- jn:px-5
- jn:py-3
+ jn:px-3
+ jn:py-2
jn:border-b
jn:border-theme-background-lvl-2
jn:h-full
+ jn:text-sm
`
}
diff --git a/packages/ui-components/src/components/DataGridHeadCell/DataGridHeadCell.component.tsx b/packages/ui-components/src/components/DataGridHeadCell/DataGridHeadCell.component.tsx
index c7544766a8..a80a2ce78f 100644
--- a/packages/ui-components/src/components/DataGridHeadCell/DataGridHeadCell.component.tsx
+++ b/packages/ui-components/src/components/DataGridHeadCell/DataGridHeadCell.component.tsx
@@ -9,8 +9,7 @@ import { DataGridCell } from "../DataGridCell/index"
const headCellBaseStyles = `
jn:font-bold
jn:text-theme-high
- jn:bg-theme-background-lvl-1
- jn:border-theme-background-lvl-0
+ jn:border-theme-default
`
/**
@@ -20,17 +19,7 @@ const headCellBaseStyles = `
* @see {@link DataGridHeadCellProps}
*/
export const DataGridHeadCell = forwardRef(
- (
- {
- // sortable,
- colSpan,
- nowrap = false,
- className = "",
- children,
- ...props
- },
- ref
- ) => {
+ ({ colSpan, nowrap = false, className = "", children, ...props }, ref) => {
return (
{children}
- //
)
}
)
@@ -56,8 +38,6 @@ export const DataGridHeadCell = forwardRef {
- /** Whether the DataGrid should be sortable by this column */
- // sortable: PropTypes.bool,
/** Add a col span to the cell. This works like a colspan in a normal html table, so you have to take care not to place too many cells in a row if some of them have a colspan. */
colSpan?: number
/** Set nowrap to true if the cell content shouldn't wrap (this is achieved by adding white-space: nowrap;) */
diff --git a/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.component.tsx b/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.component.tsx
index ada64863f5..f84c35d8b9 100644
--- a/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.component.tsx
+++ b/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.component.tsx
@@ -7,28 +7,23 @@ import React, { HTMLAttributes, ReactNode } from "react"
const baseToolbarStyles = `
jn:bg-theme-background-lvl-1
- jn:py-3
- jn:px-6
- jn:mb-px
+ jn:border-b
+ jn:border-theme-default
+ jn:py-2
+ jn:px-3
`
/**
- * `DataGridToolbar` provides an action bar for a `DataGrid`, designed to hold controls like buttons and search inputs
- * for performing group operations and interfacing with the grid content.
+ * `DataGridToolbar` is a styled wrapper for the filter, search, and state zone (Zones 2+3) of a DataGrid header.
+ * It provides a background, consistent padding, and separation from the grid below. Use `Stack` inside to compose and position content.
+ * Zone 1 content (primary actions, bulk actions, sorting) does not use this component — use a plain `Stack` there instead.
* @see https://cloudoperators.github.io/juno/?path=/docs/components-datagrid-datagridtoolbar--docs
* @see {@link DataGridToolbarProps}
*/
-export const DataGridToolbar = ({
- className = "",
- children,
- alignRight = true,
- ...props
-}: DataGridToolbarProps): ReactNode => {
- const childrenWrapperStyles = alignRight ? "jn:ml-auto" : ""
- const alignmentToolbarStyles = alignRight ? "jn:flex jn:items-center" : ""
+export const DataGridToolbar = ({ className = "", children, ...props }: DataGridToolbarProps): ReactNode => {
return (
-
-
{children}
+
+ {children}
)
}
@@ -44,12 +39,4 @@ export interface DataGridToolbarProps extends HTMLAttributes {
* @default ""
*/
className?: string
-
- /**
- * Determines whether the children are automatically aligned to the right side within the toolbar.
- * When true, applies `ml-auto` to the children wrapper, pushing content right.
- * When false, no automatic alignment is applied, allowing for custom layouts.
- * @default true
- */
- alignRight?: boolean
}
diff --git a/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.stories.tsx b/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.stories.tsx
index 237221f690..363ad612fa 100644
--- a/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.stories.tsx
+++ b/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.stories.tsx
@@ -5,17 +5,9 @@
import React from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
-import { DataGridToolbar, DataGridToolbarProps } from "./index"
-import { Pill } from "../Pill"
-import { Stack } from "../Stack"
-import { SearchInput } from "../SearchInput"
-import { ComboBox } from "../ComboBox"
-import { ComboBoxOption } from "../ComboBoxOption"
+import { DataGridToolbar } from "./index"
import { Button } from "../Button"
-import { NativeSelectOption } from "../NativeSelectOption"
-import { NativeSelect } from "../NativeSelect"
-import { InputGroup } from "../InputGroup"
-import { PortalProvider } from "../PortalProvider"
+import { Stack } from "../Stack"
const meta: Meta = {
title: "Components/DataGrid/DataGridToolbar",
@@ -23,9 +15,10 @@ const meta: Meta = {
argTypes: {
children: {
control: false,
- table: {
- type: { summary: "ReactNode" },
- },
+ table: { type: { summary: "ReactNode" } },
+ },
+ className: {
+ control: "text",
},
},
}
@@ -34,64 +27,19 @@ export default meta
type Story = StoryObj
export const Default: Story = {
- render: (args: DataGridToolbarProps) => (
+ render: (args) => (
-
-
- ),
- parameters: {
- docs: {
- description: {
- story:
- "Demonstrates a simple toolbar layout with children right-aligned by default. Use ButtonRow for multiple buttons.",
- },
- },
- },
-}
-
-export const ComplexCustomLayout: Story = {
- decorators: [
- (Story) => (
-
-
-
- ),
- ],
- render: (args: DataGridToolbarProps) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Showing 4 of 10 servers
+
),
- args: {
- alignRight: false,
- },
parameters: {
docs: {
description: {
story:
- "Demonstrates a complex toolbar layout with custom styling - children aligned left and search aligned right. The xs 'Clear Filters' button sits inline with the filter pills.",
+ "A spacing wrapper for Zone 2+3 content — filter controls, DataGrid state, and refresh. Use `className` to apply the background and `Stack` inside to position content. Zone 1 (primary actions, bulk actions, sorting) does not use `DataGridToolbar` — it is a plain `Stack`. See DataGrid Header stories for full composition examples.",
},
},
},
diff --git a/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.test.tsx b/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.test.tsx
index b413031351..790590eb8c 100644
--- a/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.test.tsx
+++ b/packages/ui-components/src/components/DataGridToolbar/DataGridToolbar.test.tsx
@@ -24,16 +24,4 @@ describe("DataGridToolbar", () => {
expect(screen.getByTestId("23")).toBeInTheDocument()
expect(screen.getByTestId("23")).toHaveAttribute("data-lolol")
})
-
- test("applies jn:ml-auto correctly when alignRight true", () => {
- render()
- expect(screen.getByTestId("my-datagridtoolbar")).toBeInTheDocument()
- expect(screen.getByTestId("my-datagridtoolbar").firstChild).toHaveClass("jn:ml-auto")
- })
-
- test("doesn't apply jn:ml-auto correctly when alignRight false", () => {
- render()
- expect(screen.getByTestId("my-datagridtoolbar")).toBeInTheDocument()
- expect(screen.getByTestId("my-datagridtoolbar").firstChild).not.toHaveClass("jn:ml-auto")
- })
})