---
id: "packages/active-ui/dashboard-module"
title: "DashboardModule"
description: "The frame of a dashboard module for lists and charts, with a scope line, rows, facts, notes, service rows and a footer action."
url: "/packages/active-ui/dashboard-module"
locale: "en"
revision: "a13d5c071128e4da"
---

# DashboardModule

`DashboardModule` is the frame of one module on a dashboard: a card with an `<h2>` title, an
optional scope line such as "Last 7 days", a body and a footer with the module's actions.
`DashboardChartModule` is the same frame for a chart. The other parts fill the body: a list of
rows, a list of facts, a note, a row per service.

**Example: A small dashboard**

Client: Runs entirely in your browser. Source: `examples/ui/dashboard-module.tsx`.

```tsx
// apps/docs/examples/ui/dashboard-module.tsx
import {
  DashboardChartModule,
  DashboardModule,
  DashboardModuleFacts,
  DashboardModuleGroupDivider,
  DashboardModuleNote,
  DashboardModuleRows,
  DashboardServiceRow,
} from '@workspace/active-ui/components/composed/dashboard-module';
import {Button} from '@workspace/active-ui/components/primitives/button';

const MONTHS = [
  {month: 'Jul', count: 42},
  {month: 'Aug', count: 35},
  {month: 'Sep', count: 58},
  {month: 'Oct', count: 49},
];

/**
 * A small dashboard: a list module with a scope and a footer action, a
 * service-status module with a fact list below a divider, an empty module
 * with a note, and a chart module. The chart is a stand-in drawn with plain
 * elements; a real one comes from a chart library. The data is fictitious.
 */
export default function DashboardModuleExample() {
  const viewAll = (
    <Button size="sm" variant="outline">
      View all
    </Button>
  );
  return (
    <div className="grid w-full gap-4 lg:grid-cols-2">
      <DashboardModule
        title="Recent changes"
        description="Last 7 days"
        footerActions={viewAll}
      >
        <DashboardModuleRows label="Recent changes">
          <li className="py-3 text-sm">Address of Ada Jansen changed</li>
          <li className="py-3 text-sm">New participant: Bram de Vries</li>
          <li className="py-3 text-sm">Scheme rates for 2027 added</li>
        </DashboardModuleRows>
      </DashboardModule>
      <DashboardModule title="Services">
        <DashboardModuleRows label="Services">
          <DashboardServiceRow
            label="Statements"
            tone="success"
            status="Running"
          />
          <DashboardServiceRow
            label="Payments"
            tone="warning"
            status="Delayed"
            meta="Checked 10:42"
            metaDateTime="2026-10-20T10:42"
            href="#payments"
          />
        </DashboardModuleRows>
        <DashboardModuleGroupDivider />
        <DashboardModuleFacts>
          <div className="flex justify-between py-3 text-sm">
            <dt className="text-muted-foreground">Last import</dt>
            <dd>Today 06:00</dd>
          </div>
        </DashboardModuleFacts>
      </DashboardModule>
      <DashboardModule title="Open questions" description="Open">
        <DashboardModuleNote>There are no open questions.</DashboardModuleNote>
      </DashboardModule>
      <DashboardChartModule title="New participants" description="Per month">
        <div
          className="flex h-40 items-end gap-3 px-4 pb-4"
          role="img"
          aria-label="New participants per month: July 42, August 35, September 58, October 49"
        >
          {MONTHS.map(({month, count}) => (
            <div
              key={month}
              className="flex flex-1 flex-col items-center gap-1"
            >
              <div
                className="bg-primary/70 w-full rounded-sm"
                style={{height: `${count * 2}px`}}
              />
              <span className="text-muted-foreground text-xs">{month}</span>
            </div>
          ))}
        </div>
      </DashboardChartModule>
    </div>
  );
}
```

## The pieces

- `DashboardModule`: title, `description` (the scope of the selection, not an explanation),
  `footerActions` (zero to two buttons; no footer without them) and the body. A minimum height from
  the `lg` width keeps a short module from collapsing.
- `DashboardChartModule`: title, scope and the chart; no footer. The chart itself comes from your
  chart library.
- `DashboardModuleRows`: a `<ul>` named by `label`, with lines between the rows. Rows are `<li>`
  elements without their own horizontal padding.
- `DashboardModuleFacts`: the same lines with `<dl>` semantics, for established values.
- `DashboardModuleNote`: a muted line for "there is nothing" or "it could not be read".
- `DashboardModuleGroupDivider`: a line between two groups in one module, hidden from assistive
  technology.
- `DashboardServiceRow`: a service on the left and one [StatusBadge](/packages/active-ui/status-badge)
  on the right, with an optional provenance line (`meta`, a `<time>` with `metaDateTime`) and a link.

## Usage

```tsx
import {
  DashboardModule,
  DashboardModuleRows,
} from '@workspace/active-ui/components/composed/dashboard-module';

<DashboardModule
  title="Recent changes"
  description="Last 7 days"
  footerActions={viewAll}
>
  <DashboardModuleRows label="Recent changes">
    {changes.map((change) => (
      <li key={change.id} className="py-3 text-sm">
        {change.text}
      </li>
    ))}
  </DashboardModuleRows>
</DashboardModule>;
```

## What your application provides

- The titles, scope lines, rows and actions, translated and formatted; the mapping from service
  states to tones.
- The grid around the modules; `lg:auto-rows-fr` gives a row of modules one height.
- The chart, with its own accessible description.

## Accessibility

- Each module title is an `<h2>`, so the dashboard reads as sections below the page's `<h1>`.
- A row list needs its `label`: the module title is outside the list.
- A service row's state is a word in the badge, not only a colour.

## API

**DashboardModule**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | The body: `DashboardModuleRows`, `DashboardModuleFacts` or a note. |
| `title` | `string` | yes | The already translated module title. Renders as `<h2>`. |
| `className` | `string` | no | For placement (`lg:col-span-2`), not for meaning. |
| `description` | `string` | no | The **scope of the selection**, already translated. Short, and only what a reader needs to know which rows they are seeing: "Last 7 days", "Open", "Financial year 2026".  **The same strict test as for {@link DashboardChartModuleProps.description}, and on the same grounds.** If the sentence changes the meaning of the selection, it is scope and belongs here. If it explains what a notification is, how the module works or what the reader should conclude, it is commentary and does not belong here. No help text, no instructions, no warnings.  **Why a list module carries it too.** A list module labels its fields with columns and rows, but not its selection: a bounded summary over a rolling window shows three rows without saying anywhere that there is a window, and then an empty list reads as "nothing has ever happened" instead of "nothing in the last seven days". That is the same unnameability for which the chart module carries the line.  Omitting it is the norm. A module whose set is complete, or whose selection follows from its title, does not get it. |
| `footerActions` | `ReactNode` | no | The actions that belong to the **whole module**, bottom left in a footer zone of their own. Zero, one or two buttons side by side.  Omit when there is no destination: the footer zone then does not appear, which is better than an empty strip or a dead button. An action that belongs to one row sits in that row and never ends up here. |

**DashboardChartModule**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | The chart. This shell renders nothing of the visualisation itself. |
| `title` | `string` | yes | The already translated module title. Renders as `<h2>`, as for a list. |
| `className` | `string` | no | For placement (`lg:col-span-2`), not for meaning. |
| `description` | `string` | no | The **scope of the visualisation**, already translated. Short, and only what a reader needs in order to interpret the chart.  What belongs here: "Last 7 days", "Per month", "Financial year 2026", "Active participants per status". Each one a statement about which data is shown, and without that statement the chart is ambiguous - a bar chart of mutations says nothing as long as the period is unknown.  What does **not** belong here: help text, instructions, warnings, a long explanation or promotional copy. The test is strict and simple: if the sentence changes the **meaning of the axes or the selection**, it is scope; if it explains what the reader should conclude from the chart, it is commentary and does not belong here.  **Why the chart module carries the line.** A chart has no column headers and cannot name its own selection; there the line is content rather than commentary. {@link DashboardModule} carries it under the same test (see {@link DashboardModuleProps.description}). What stays excluded on both is a free explanatory line. |

**DashboardModuleGroupDivider**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

DashboardModuleGroupDivider has no props of its own.

**DashboardModuleRows**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | `<li>` elements. Give them **no `px-*` of their own**: it is on the list. |
| `label` | `string` | yes | The accessible name of the list. Required, not optional: the module title sits outside the list, so without a name a screen reader announces "list with 5 items" without saying of what. |
| `className` | `string` | no | Classes for the list. |

**DashboardModuleFacts**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | `<div>` groups with a `<dt>` and a `<dd>`. No `px-*` of their own. |
| `className` | `string` | no | Classes for the list. |

**DashboardModuleNote**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes | The translated notice. |

**DashboardServiceRow**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | yes | The already translated service label, on the left. This component does not translate. |
| `status` | `string` | yes | The already translated state label, on the right in the badge. Required: colour must never be the only carrier of meaning (WCAG 1.4.1). |
| `tone` | `StatusTone` | yes | The generic UI tone of the state. Mapping from the app's state model is the caller's job: this component does not know what `degraded`, `OFF` or `unknown` mean. |
| `href` | `string` | no | The destination of this service, when a detail surface actually exists. The label then becomes a link; the row gets no chevron, no extra action text and no second hint. Omitting it yields plain text: a row without a destination does not pretend to have one. |
| `linkComponent` | `LinkComponent` | no | The host's link for {@link DashboardServiceRowProps.href}, for example `next/link` so navigation stays client-side. Without it the label is a plain `<a href>`. Only meaningful with `href`. |
| `meta` | `string` | no | An already translated, short provenance line for the state: when it was established, or that the evidence is missing or stale. Muted, to the left of the badge. This is not a measurement and not a second status word: it says nothing about the service and everything about the evidence. Omitting it draws nothing. |
| `metaDateTime` | `string` | no | The machine-readable moment behind `meta`, ISO 8601, for the `dateTime` attribute of `<time>`. Only meaningful together with `meta`. |

**DashboardModuleProps**

```ts
interface DashboardModuleProps {
  title: string;
  description?: string;
  footerActions?: React.ReactNode;
  className?: string;
  children: React.ReactNode;
}
```

**DashboardChartModuleProps**

```ts
interface DashboardChartModuleProps {
  title: string;
  description?: string;
  className?: string;
  children: React.ReactNode;
}
```

**DashboardModuleRowsProps**

```ts
interface DashboardModuleRowsProps {
  label: string;
  children: React.ReactNode;
  className?: string;
}
```

**DashboardModuleFactsProps**

```ts
interface DashboardModuleFactsProps {
  children: React.ReactNode;
  className?: string;
}
```

**DashboardServiceRowProps**

```ts
interface DashboardServiceRowProps {
  label: string;
  tone: StatusTone;
  status: string;
  href?: string;
  linkComponent?: LinkComponent;
  meta?: string;
  metaDateTime?: string;
}
```
