Skip to content

Styling & themes ​

Themes ​

The grid ships a light and a dark theme. theme="auto" follows the operating system.

vue
<Datagrid theme="dark" ... />
<Datagrid theme="auto" ... />
Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label><input v-model="theme" type="radio" value="light" /> Light</label>
      <label><input v-model="theme" type="radio" value="dark" /> Dark</label>
      <label><input v-model="theme" type="radio" value="auto" /> System</label>
      <label><input v-model="custom" type="checkbox" /> Custom variables</label>
    </div>
    <Datagrid
      :class="{ 'my-grid': custom }"
      :theme="theme"
      :columns="columns"
      :rows="rows"
      :options="options"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const theme = ref<"light" | "dark" | "auto">("light");
const custom = ref(true);
const rows = createEmployees(200);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
  { field: "active", text: "Active", type: "boolean" },
];

const options: GridOptions<Employee> = {
  checkboxSelection: true,
  totalRow: "bottom",
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>

<style>
.uni-table.my-grid {
  --ut-accent-color: #7c3aed;
  --ut-font-size: 13px;
  --ut-cell-padding-y: 3px;
  --ut-header-row-height: 32px;
  --ut-radius: 12px;
  --ut-header-background: color-mix(in srgb, #7c3aed 8%, var(--ut-background));
}
</style>
tsx
import { useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

type Theme = "light" | "dark" | "auto";

const rows = createEmployees(200);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
  { field: "active", text: "Active", type: "boolean" },
];

const options: GridOptions<Employee> = {
  checkboxSelection: true,
  totalRow: "bottom",
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function Theming() {
  const [theme, setTheme] = useState<Theme>("light");
  const [custom, setCustom] = useState(true);

  const themeRadio = (value: Theme, label: string) => (
    <label>
      <input type="radio" checked={theme === value} onChange={() => setTheme(value)} /> {label}
    </label>
  );

  return (
    <div className="demo">
      <div className="demo-toolbar">
        {themeRadio("light", "Light")}
        {themeRadio("dark", "Dark")}
        {themeRadio("auto", "System")}
        <label>
          <input type="checkbox" checked={custom} onChange={(e) => setCustom(e.target.checked)} /> Custom variables
        </label>
      </div>
      <Datagrid
        className={custom ? "my-grid" : undefined}
        theme={theme}
        columns={columns}
        rows={rows}
        options={options}
      />
      <style>{`
        .uni-table.my-grid {
          --ut-accent-color: #7c3aed;
          --ut-font-size: 13px;
          --ut-cell-padding-y: 3px;
          --ut-header-row-height: 32px;
          --ut-radius: 12px;
          --ut-header-background: color-mix(in srgb, #7c3aed 8%, var(--ut-background));
        }
      `}</style>
    </div>
  );
}

CSS custom properties ​

All colors and sizes are CSS custom properties defined on the element with the class uni-table. Override them for all grids or for a single grid:

css
/* all grids */
.uni-table {
  --ut-accent-color: #0969da;
  --ut-font-size: 13px;
}

/* a single grid: <Datagrid class="compact" /> */
.uni-table.compact {
  --ut-cell-padding-y: 2px;
  --ut-header-row-height: 30px;
}

Colors ​

VariableDescription
--ut-accent-colorselection, focus, sort icons, active states
--ut-accent-contrast-colortext on accent colored backgrounds
--ut-focus-coloroutline of the focused cell
--ut-color / --ut-muted-colortext colors
--ut-backgroundbackground of the grid
--ut-header-background / --ut-header-colorheader, filter row, group area, pagination
--ut-border-colorouter border, header borders
--ut-row-border-colorlines between rows
--ut-row-background / --ut-row-alt-backgroundeven / odd rows
--ut-row-hover-backgroundhovered row
--ut-selection-background / --ut-selection-hover-backgroundselected rows
--ut-range-selection-backgroundselected cells
--ut-group-backgroundgroup rows
--ut-total-backgroundpinned and total rows
--ut-icon-coloricons
--ut-input-background / --ut-input-border-colorinputs of filters and editors
--ut-popup-background / --ut-popup-shadowmenus and filter popups
--ut-pinned-shadowshadow of pinned columns

Sizes and typography ​

VariableDefaultDescription
--ut-font-familyinherit
--ut-font-size14px
--ut-line-height20px
--ut-header-font-weight600
--ut-cell-padding-x / --ut-cell-padding-y8px / 6pxpadding of cells (defines the row height if rows are measured)
--ut-header-row-height38pxheight of header rows (also grid option headerRowHeight)
--ut-filter-row-height36pxheight of the filter row (also grid option filterRowHeight)
--ut-radius6pxborder radius of the grid
--ut-icon-size14px
--ut-cell-bordernonevertical cell borders, e.g. 1px solid var(--ut-row-border-color)

Density ​

Switch the density with a class that sets the size variables. A compact grid shows more rows for data-heavy screens (spreadsheets, logs), a comfortable grid is easier to read and to use on touch devices. Row heights follow automatically because rows are measured.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label v-for="value in densities" :key="value">
        <input v-model="density" type="radio" :value="value" /> {{ value }}
      </label>
    </div>
    <Datagrid :class="`sd-${density}`" :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const densities = ["compact", "normal", "comfortable"];
const density = ref("compact");
const rows = createEmployees(200);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "city", text: "City" },
  { field: "salary", text: "Salary", type: "number" },
];

const options: GridOptions<Employee> = {
  checkboxSelection: true,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>

<style>
/* the row height follows the padding and line height, because rows are measured */
.uni-table.sd-compact {
  --ut-font-size: 12px;
  --ut-line-height: 16px;
  --ut-cell-padding-y: 2px;
  --ut-cell-padding-x: 6px;
  --ut-header-row-height: 28px;
  --ut-filter-row-height: 30px;
}
.uni-table.sd-comfortable {
  --ut-font-size: 15px;
  --ut-line-height: 22px;
  --ut-cell-padding-y: 12px;
  --ut-cell-padding-x: 14px;
  --ut-header-row-height: 48px;
  --ut-filter-row-height: 44px;
}
</style>
tsx
import { useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const densities = ["compact", "normal", "comfortable"];
const rows = createEmployees(200);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "city", text: "City" },
  { field: "salary", text: "Salary", type: "number" },
];

const options: GridOptions<Employee> = {
  checkboxSelection: true,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function StylingDensity() {
  const [density, setDensity] = useState("compact");

  return (
    <div className="demo">
      <div className="demo-toolbar">
        {densities.map((value) => (
          <label key={value}>
            <input type="radio" checked={density === value} onChange={() => setDensity(value)} /> {value}
          </label>
        ))}
      </div>
      <Datagrid className={`sd-${density}`} columns={columns} rows={rows} options={options} />
      <style>{`
        /* the row height follows the padding and line height, because rows are measured */
        .uni-table.sd-compact {
          --ut-font-size: 12px;
          --ut-line-height: 16px;
          --ut-cell-padding-y: 2px;
          --ut-cell-padding-x: 6px;
          --ut-header-row-height: 28px;
          --ut-filter-row-height: 30px;
        }
        .uni-table.sd-comfortable {
          --ut-font-size: 15px;
          --ut-line-height: 22px;
          --ut-cell-padding-y: 12px;
          --ut-cell-padding-x: 14px;
          --ut-header-row-height: 48px;
          --ut-filter-row-height: 44px;
        }
      `}</style>
    </div>
  );
}

Custom themes ​

A brand theme is a set of variables on a class. Start with the colors you need – usually --ut-accent-color, the header colors and the row backgrounds – and add sizes, fonts or vertical cell borders (--ut-cell-border). A dark brand theme uses theme="dark" as base and overrides only a few colors.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      Theme
      <select v-model="brand">
        <option value="ocean">Ocean</option>
        <option value="sunset">Sunset</option>
        <option value="midnight">Midnight (dark)</option>
      </select>
    </div>
    <!-- a dark brand theme builds on the dark theme and only changes a few colors -->
    <Datagrid
      :class="`sb-${brand}`"
      :theme="brand === 'midnight' ? 'dark' : 'light'"
      :columns="columns"
      :rows="rows"
      :options="options"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const brand = ref("ocean");
const rows = createEmployees(200);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum", valueFormatter: formatters.number() },
  { field: "remote", text: "Remote", type: "boolean" },
];

const options: GridOptions<Employee> = {
  checkboxSelection: true,
  totalRow: "bottom",
  columnMenu: true,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>

<style>
.uni-table.sb-ocean {
  --ut-accent-color: #0550ae;
  --ut-header-background: #0969da;
  --ut-header-color: #ffffff;
  --ut-icon-color: #0550ae;
  --ut-row-alt-background: #f3f8ff;
  --ut-selection-background: #ddf4ff;
  --ut-cell-border: 1px solid var(--ut-row-border-color);
  --ut-radius: 0;
}
/* icons in the colored header */
.uni-table.sb-ocean .ut-header {
  --ut-icon-color: #ffffff;
}
.uni-table.sb-sunset {
  --ut-accent-color: #d1480b;
  --ut-header-background: #fff4ec;
  --ut-header-color: #8a2d05;
  --ut-row-border-color: #fbe3d3;
  --ut-row-alt-background: #fffaf6;
  --ut-selection-background: #ffe6d5;
  --ut-total-background: #fff4ec;
  --ut-font-family: Georgia, serif;
  --ut-font-size: 13px;
  --ut-radius: 14px;
}
.uni-table.sb-midnight {
  --ut-accent-color: #a371f7;
  --ut-background: #0d1021;
  --ut-header-background: #161a33;
  --ut-row-alt-background: #11152a;
  --ut-row-border-color: #242a4a;
  --ut-selection-background: #2c2356;
  --ut-total-background: #161a33;
}
</style>
tsx
import { useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(200);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum", valueFormatter: formatters.number() },
  { field: "remote", text: "Remote", type: "boolean" },
];

const options: GridOptions<Employee> = {
  checkboxSelection: true,
  totalRow: "bottom",
  columnMenu: true,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function StylingBrand() {
  const [brand, setBrand] = useState("ocean");

  return (
    <div className="demo">
      <div className="demo-toolbar">
        Theme
        <select value={brand} onChange={(e) => setBrand(e.target.value)}>
          <option value="ocean">Ocean</option>
          <option value="sunset">Sunset</option>
          <option value="midnight">Midnight (dark)</option>
        </select>
      </div>
      {/* a dark brand theme builds on the dark theme and only changes a few colors */}
      <Datagrid
        className={`sb-${brand}`}
        theme={brand === "midnight" ? "dark" : "light"}
        columns={columns}
        rows={rows}
        options={options}
      />
      <style>{`
        .uni-table.sb-ocean {
          --ut-accent-color: #0550ae;
          --ut-header-background: #0969da;
          --ut-header-color: #ffffff;
          --ut-icon-color: #0550ae;
          --ut-row-alt-background: #f3f8ff;
          --ut-selection-background: #ddf4ff;
          --ut-cell-border: 1px solid var(--ut-row-border-color);
          --ut-radius: 0;
        }
        /* icons in the colored header */
        .uni-table.sb-ocean .ut-header {
          --ut-icon-color: #ffffff;
        }
        .uni-table.sb-sunset {
          --ut-accent-color: #d1480b;
          --ut-header-background: #fff4ec;
          --ut-header-color: #8a2d05;
          --ut-row-border-color: #fbe3d3;
          --ut-row-alt-background: #fffaf6;
          --ut-selection-background: #ffe6d5;
          --ut-total-background: #fff4ec;
          --ut-font-family: Georgia, serif;
          --ut-font-size: 13px;
          --ut-radius: 14px;
        }
        .uni-table.sb-midnight {
          --ut-accent-color: #a371f7;
          --ut-background: #0d1021;
          --ut-header-background: #161a33;
          --ut-row-alt-background: #11152a;
          --ut-row-border-color: #242a4a;
          --ut-selection-background: #2c2356;
          --ut-total-background: #161a33;
        }
      `}</style>
    </div>
  );
}

Row height ​

Without rowHeight every row is measured, so rows can have different heights (multi line text with autoHeight, custom components). With a fixed rowHeight (e.g. 32) no measuring is necessary, which is faster for very large grids.

CSS classes ​

The most important classes to write your own styles:

ClassElement
.uni-tableroot element
.ut-header-cellheader cell (.ut-sorted, .ut-filtered, .ut-pinned-left, …)
.ut-filter-cellcell of the filter row
.ut-rowrow (.odd, .even, .selected, .hovered, .focused, .ut-row-pinned, .ut-row-total)
.ut-groupgroup row
.ut-cellcell (.selected, .focused, .ut-editable, .ut-pinned-left, .left / .center / .right)
.ut-editingeditor
.ut-paginationpagination bar
.ut-popupmenus and filter popups

Dark mode of your application

If your application switches the color scheme with a class (e.g. html.dark), set the variables for this class or use :theme="isDark ? 'dark' : 'light'".

Conditional formatting ​

Row classes ​

rowClass (and rowStyle) style complete rows depending on the data – overdue tasks, archived records, rows with errors. rowClass returns a class name or an array of class names. Set row colors with the variables --ut-row-background / --ut-row-alt-background on the row class: hover and selection keep working.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <span class="sr-legend sr-overdue">overdue</span>
      <span class="sr-legend sr-done">done</span>
      <span class="sr-legend sr-high">high priority</span>
      <span>(today is {{ TODAY }})</span>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, TODAY, type Task } from "./data";

const rows = createTasks(100);

const isOverdue = (task: Task) => !task.done && task.due < TODAY;

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", flex: 1 },
  { field: "assignee", text: "Assignee", width: 150 },
  { field: "status", text: "Status", width: 110 },
  { field: "priority", text: "Priority", width: 90 },
  {
    field: "due",
    text: "Due",
    type: "date",
    width: 150,
    valueFormatter: formatters.date({ dateStyle: "medium" }),
    // cell classes are added to the row classes
    cellClass: ({ data }) => (isOverdue(data) ? "sr-due-overdue" : undefined),
  },
];

const options: GridOptions<Task> = {
  // one or more classes per row, depending on the data
  rowClass: (row) => [
    isOverdue(row.data) ? "sr-overdue" : "",
    row.data.done ? "sr-done" : "",
  ],
  // inline styles work as well
  rowStyle: (row) => (row.data.priority === "high" ? { fontWeight: "600" } : undefined),
  defaultColumn: { sortable: true },
};
</script>

<style>
.uni-table .sr-overdue {
  --ut-row-background: color-mix(in srgb, #cf222e 10%, var(--ut-background));
  --ut-row-alt-background: var(--ut-row-background);
  color: #cf222e;
}
.uni-table .sr-done {
  color: var(--ut-muted-color);
  text-decoration: line-through;
}
.sr-due-overdue::after {
  content: "⚠";
  margin-left: 6px;
}
.sr-legend {
  padding: 1px 8px;
  border-radius: 4px;
}
.sr-legend.sr-overdue {
  background: color-mix(in srgb, #cf222e 12%, transparent);
  color: #cf222e;
}
.sr-legend.sr-done {
  color: var(--vp-c-text-2);
  text-decoration: line-through;
}
.sr-legend.sr-high {
  font-weight: 600;
}
</style>
tsx
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createTasks, TODAY, type Task } from "../data";

const rows = createTasks(100);

const isOverdue = (task: Task) => !task.done && task.due < TODAY;

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", flex: 1 },
  { field: "assignee", text: "Assignee", width: 150 },
  { field: "status", text: "Status", width: 110 },
  { field: "priority", text: "Priority", width: 90 },
  {
    field: "due",
    text: "Due",
    type: "date",
    width: 150,
    valueFormatter: formatters.date({ dateStyle: "medium" }),
    // cell classes are added to the row classes
    cellClass: ({ data }) => (isOverdue(data) ? "sr-due-overdue" : undefined),
  },
];

const options: GridOptions<Task> = {
  // one or more classes per row, depending on the data
  rowClass: (row) => [
    isOverdue(row.data) ? "sr-overdue" : "",
    row.data.done ? "sr-done" : "",
  ],
  // inline styles work as well
  rowStyle: (row) => (row.data.priority === "high" ? { fontWeight: "600" } : undefined),
  defaultColumn: { sortable: true },
};

export default function StylingRowClass() {
  return (
    <div className="demo">
      <div className="demo-toolbar">
        <span className="sr-legend sr-overdue">overdue</span>
        <span className="sr-legend sr-done">done</span>
        <span className="sr-legend sr-high">high priority</span>
        <span>(today is {TODAY})</span>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} />
      <style>{`
        .uni-table .sr-overdue {
          --ut-row-background: color-mix(in srgb, #cf222e 10%, var(--ut-background));
          --ut-row-alt-background: var(--ut-row-background);
          color: #cf222e;
        }
        .uni-table .sr-done {
          color: var(--ut-muted-color);
          text-decoration: line-through;
        }
        .sr-due-overdue::after {
          content: "⚠";
          margin-left: 6px;
        }
        .sr-legend {
          padding: 1px 8px;
          border-radius: 4px;
        }
        .sr-legend.sr-overdue {
          background: color-mix(in srgb, #cf222e 12%, transparent);
          color: #cf222e;
        }
        .sr-legend.sr-done {
          color: var(--vp-c-text-2);
          text-decoration: line-through;
        }
        .sr-legend.sr-high {
          font-weight: 600;
        }
      `}</style>
    </div>
  );
}

Results of rowClass, rowStyle, cellClass and cellStyle are cached and recalculated when the data of the row changes.

Specificity

The grid styles cells with selectors like .uni-table .ut-cell. Prefix your own rules with .uni-table (e.g. .uni-table .overdue) if they set properties the grid sets as well, like background-color.

Cell styles and heatmaps ​

cellClass and cellStyle style single cells by their value. Classes are best for a few states (positive / negative), cellStyle for continuous values like the background of a heatmap. The demo mixes the color with --ut-current-row-background, the background of the row including hover and selection.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label><input v-model="heatmap" type="checkbox" /> Heatmap</label>
      <span class="sh-scale">low <span class="sh-scale__bar"></span> high</span>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createRandom } from "./data";

type Sales = { region: string; growth: number } & Record<string, number>;

const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const regions = ["North", "East", "South", "West", "Central", "Alps", "Coast", "Islands"];

const random = createRandom(3);
const rows = regions.map((region) => {
  const row = { region, growth: Math.round((random() - 0.4) * 300) / 10 } as Sales;
  months.forEach((month, i) => (row[month] = Math.round(40 + random() * 60 + Math.sin(i / 2) * 25)));
  return row;
});

const heatmap = ref(true);

// value between min and max -> background with 0-60 % of the accent color
const values = rows.flatMap((row) => months.map((month) => row[month]));
const min = Math.min(...values);
const max = Math.max(...values);
const heat = (value: number) => Math.round(((value - min) / (max - min)) * 60);

const columns = computed<Array<ColumnConfig<Sales>>>(() => [
  { field: "region", text: "Region", width: 100, pinned: "left" },
  ...months.map((month) => ({
    field: month,
    text: month,
    type: "number",
    width: 62,
    cellStyle: heatmap.value
      ? ({ value }: { value: number }) => ({
          backgroundColor: `color-mix(in srgb, #2e9e6e ${heat(value)}%, var(--ut-current-row-background))`,
        })
      : undefined,
  })),
  {
    field: "growth",
    text: "Growth",
    type: "number",
    width: 90,
    pinned: "right",
    valueFormatter: ({ value }) => `${value > 0 ? "▲" : "▼"} ${Math.abs(value)} %`,
    // classes for conditional formatting, styled with css
    cellClass: ({ value }) => (value > 10 ? "sh-strong" : value < 0 ? "sh-negative" : "sh-positive"),
  },
]);

const options: GridOptions<Sales> = {
  defaultColumn: { sortable: true },
};
</script>

<style>
.uni-table .sh-positive {
  color: #1a7f37;
}
.uni-table .sh-strong {
  color: #1a7f37;
  font-weight: 700;
}
.uni-table .sh-negative {
  color: #cf222e;
}
.sh-scale {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
}
.sh-scale__bar {
  width: 80px;
  height: 10px;
  border-radius: 2px;
  background: linear-gradient(to right, transparent, color-mix(in srgb, #2e9e6e 60%, transparent));
  border: 1px solid var(--vp-c-divider);
}
</style>
tsx
import { useMemo, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createRandom } from "../data";

type Sales = { region: string; growth: number } & Record<string, number>;

const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const regions = ["North", "East", "South", "West", "Central", "Alps", "Coast", "Islands"];

const random = createRandom(3);
const rows = regions.map((region) => {
  const row = { region, growth: Math.round((random() - 0.4) * 300) / 10 } as Sales;
  months.forEach((month, i) => (row[month] = Math.round(40 + random() * 60 + Math.sin(i / 2) * 25)));
  return row;
});

// value between min and max -> background with 0-60 % of the accent color
const values = rows.flatMap((row) => months.map((month) => row[month]));
const min = Math.min(...values);
const max = Math.max(...values);
const heat = (value: number) => Math.round(((value - min) / (max - min)) * 60);

function createColumns(heatmap: boolean): Array<ColumnConfig<Sales>> {
  return [
    { field: "region", text: "Region", width: 100, pinned: "left" },
    ...months.map((month) => ({
      field: month,
      text: month,
      type: "number",
      width: 62,
      cellStyle: heatmap
        ? ({ value }: { value: number }) => ({
            backgroundColor: `color-mix(in srgb, #2e9e6e ${heat(value)}%, var(--ut-current-row-background))`,
          })
        : undefined,
    })),
    {
      field: "growth",
      text: "Growth",
      type: "number",
      width: 90,
      pinned: "right",
      valueFormatter: ({ value }) => `${value > 0 ? "▲" : "▼"} ${Math.abs(value)} %`,
      // classes for conditional formatting, styled with css
      cellClass: ({ value }) => (value > 10 ? "sh-strong" : value < 0 ? "sh-negative" : "sh-positive"),
    },
  ];
}

const options: GridOptions<Sales> = {
  defaultColumn: { sortable: true },
};

export default function StylingHeatmap() {
  const [heatmap, setHeatmap] = useState(true);
  const columns = useMemo(() => createColumns(heatmap), [heatmap]);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          <input type="checkbox" checked={heatmap} onChange={(e) => setHeatmap(e.target.checked)} /> Heatmap
        </label>
        <span className="sh-scale">
          low <span className="sh-scale__bar"></span> high
        </span>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} />
      <style>{`
        .uni-table .sh-positive {
          color: #1a7f37;
        }
        .uni-table .sh-strong {
          color: #1a7f37;
          font-weight: 700;
        }
        .uni-table .sh-negative {
          color: #cf222e;
        }
        .sh-scale {
          display: inline-flex;
          align-items: center;
          gap: 6px;
          font-size: 12px;
        }
        .sh-scale__bar {
          width: 80px;
          height: 10px;
          border-radius: 2px;
          background: linear-gradient(to right, transparent, color-mix(in srgb, #2e9e6e 60%, transparent));
          border: 1px solid var(--vp-c-divider);
        }
      `}</style>
    </div>
  );
}

SCSS ​

The source of the styles is available as @datagrid/core/style.scss if you want to include it in your own SCSS build.

Released under the ISC License.