Appearance
Row grouping & aggregation
Rows can be grouped by one or more columns. Group rows can be expanded and collapsed and show aggregated values of their rows.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<button @click="groups = ['country']">Group by country</button>
<button @click="groups = ['country', 'department']">Country › Department</button>
<button @click="groups = []">No grouping</button>
</div>
<Datagrid
:columns="columns"
:rows="rows"
:options="options"
v-model:columnGroups="groups"
/>
</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 rows = createEmployees(1000);
const groups = ref(["country", "department"]);
const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });
const columns: Array<ColumnConfig<Employee>> = [
{ field: "country", text: "Country", groupable: true },
{ field: "department", text: "Department", groupable: true },
{ field: "role", text: "Role", groupable: true },
{ field: "lastName", text: "Name" },
{ field: "salary", text: "Salary (sum)", type: "number", aggFunc: "sum", valueFormatter: currency },
{ field: "bonus", text: "Bonus (avg)", type: "number", aggFunc: "avg", valueFormatter: currency },
{ field: "rating", text: "Rating (max)", type: "number", aggFunc: "max" },
{
field: "remote",
text: "Remote (%)",
type: "number",
// custom aggregation function
aggFunc: (values) => values.filter(Boolean).length / Math.max(1, values.length),
valueFormatter: ({ value }) =>
typeof value === "number" ? `${Math.round(value * 100)} %` : value ? "yes" : "no",
},
];
const options: GridOptions<Employee> = {
groupDefaultExpanded: 1,
totalRow: "bottom",
columnMenu: true,
defaultColumn: { sortable: true, resizeable: true, width: 150 },
};
</script>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(1000);
const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });
const columns: Array<ColumnConfig<Employee>> = [
{ field: "country", text: "Country", groupable: true },
{ field: "department", text: "Department", groupable: true },
{ field: "role", text: "Role", groupable: true },
{ field: "lastName", text: "Name" },
{ field: "salary", text: "Salary (sum)", type: "number", aggFunc: "sum", valueFormatter: currency },
{ field: "bonus", text: "Bonus (avg)", type: "number", aggFunc: "avg", valueFormatter: currency },
{ field: "rating", text: "Rating (max)", type: "number", aggFunc: "max" },
{
field: "remote",
text: "Remote (%)",
type: "number",
// custom aggregation function
aggFunc: (values) => values.filter(Boolean).length / Math.max(1, values.length),
valueFormatter: ({ value }) =>
typeof value === "number" ? `${Math.round(value * 100)} %` : value ? "yes" : "no",
},
];
const options: GridOptions<Employee> = {
groupDefaultExpanded: 1,
totalRow: "bottom",
columnMenu: true,
defaultColumn: { sortable: true, resizeable: true, width: 150 },
};
export default function Grouping() {
const [groups, setGroups] = useState(["country", "department"]);
return (
<div className="demo">
<div className="demo-toolbar">
<button onClick={() => setGroups(["country"])}>Group by country</button>
<button onClick={() => setGroups(["country", "department"])}>Country › Department</button>
<button onClick={() => setGroups([])}>No grouping</button>
</div>
<Datagrid
columns={columns}
rows={rows}
options={options}
columnGroups={groups}
onColumnGroupsChange={setGroups}
/>
</div>
);
}Grouping rows
Columns with groupable: true can be dragged into the group area above the header (the area is shown as soon as one column is groupable). The column menu offers "Group by this column" as well.
- Click a group chip to toggle the sort direction of the groups.
- Drag a chip to change the order of the grouping levels.
- Drag a chip out of the group area (or click ×) to remove the grouping.
Programmatically:
vue
<Datagrid v-model:columnGroups="groups" ... />tsx
<Datagrid columnGroups={groups} onColumnGroupsChange={setGroups} ... />ts
api.setRowGrouping(["country"]);
api.addRowGrouping("department");
api.deleteRowGrouping("country");
api.moveRowGrouping("department", 0);
api.toggleRowGroupingSort("country");
api.clearRowGrouping();Groups are sorted ascending by their key. Empty values are grouped in a group displayed as "–".
Letting users group
Leave the grouping empty and mark the columns users may group by as groupable. This is the typical setup for ad-hoc analysis: users build their own report by dragging headers into the group area. The bound columnGroups model shows the current grouping.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<span>Drag a header (Country, Department, Role, City) into the area above the header.</span>
<button @click="groups = []">Clear grouping</button>
</div>
<Datagrid :columns="columns" :rows="rows" :options="options" v-model:columnGroups="groups" />
<code class="demo-log">columnGroups: {{ JSON.stringify(groups) }}</code>
</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 rows = createEmployees(500);
// no initial grouping: the user decides
const groups = ref<Array<string>>([]);
const columns: Array<ColumnConfig<Employee>> = [
// only groupable columns can be dropped into the group area
{ field: "country", text: "Country", groupable: true },
{ field: "department", text: "Department", groupable: true },
{ field: "role", text: "Role", groupable: true },
{ field: "city", text: "City", groupable: true },
{ field: "lastName", text: "Name" },
{ field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
];
const options: GridOptions<Employee> = {
columnMenu: true, // the column menu offers "Group by this column" too
defaultColumn: { sortable: true, moveable: true, flex: 1 },
};
</script>tsx
import { useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";
const rows = createEmployees(500);
const columns: Array<ColumnConfig<Employee>> = [
// only groupable columns can be dropped into the group area
{ field: "country", text: "Country", groupable: true },
{ field: "department", text: "Department", groupable: true },
{ field: "role", text: "Role", groupable: true },
{ field: "city", text: "City", groupable: true },
{ field: "lastName", text: "Name" },
{ field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
];
const options: GridOptions<Employee> = {
columnMenu: true, // the column menu offers "Group by this column" too
defaultColumn: { sortable: true, moveable: true, flex: 1 },
};
export default function GroupingDragDrop() {
// no initial grouping: the user decides
const [groups, setGroups] = useState<Array<string>>([]);
return (
<div className="demo">
<div className="demo-toolbar">
<span>Drag a header (Country, Department, Role, City) into the area above the header.</span>
<button onClick={() => setGroups([])}>Clear grouping</button>
</div>
<Datagrid
columns={columns}
rows={rows}
options={options}
columnGroups={groups}
onColumnGroupsChange={setGroups}
/>
<code className="demo-log">columnGroups: {JSON.stringify(groups)}</code>
</div>
);
}Expanding and collapsing
ts
groupDefaultExpanded: 1 // grid option: number of expanded levels for new groups, -1 = allts
api.expandAll();
api.collapseAll();
api.setExpandedKeys(keys); // or v-model:expandedItemsGroup rows are toggled by clicking them. The expanded state of a group is kept when the data changes.
Choose 0 for a compact overview of many groups, 1 to show the next level of nested groups and -1 when users mainly read the rows. groupShowCount: false hides the number of rows behind the group key. groupDefaultExpanded only applies to groups which are created, so the demo recreates the grid when it changes.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<label>
groupDefaultExpanded
<select v-model.number="expanded">
<option :value="0">0 – all collapsed</option>
<option :value="1">1 – first level expanded</option>
<option :value="-1">-1 – all expanded</option>
</select>
</label>
<label><input v-model="showCount" type="checkbox" /> groupShowCount</label>
<button @click="grid?.expandAll()">Expand all</button>
<button @click="grid?.collapseAll()">Collapse all</button>
</div>
<!-- groupDefaultExpanded applies to new groups, the key recreates the grid -->
<Datagrid
ref="grid"
:key="expanded"
:columns="columns"
:rows="rows"
:options="options"
:columnGroups="groups"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, type Task } from "./data";
const rows = createTasks(150);
const groups = ["project", "status"];
const grid = ref<InstanceType<typeof Datagrid>>();
const expanded = ref(1);
const showCount = ref(true);
const columns: Array<ColumnConfig<Task>> = [
{ field: "project", text: "Project", groupable: true },
{ field: "status", text: "Status", groupable: true },
{ field: "title", text: "Task" },
{ field: "assignee", text: "Assignee" },
{ field: "estimate", text: "Estimate (h)", type: "number", aggFunc: "sum" },
];
const options = computed<GridOptions<Task>>(() => ({
groupDefaultExpanded: expanded.value,
groupShowCount: showCount.value,
defaultColumn: { flex: 1 },
}));
</script>tsx
import { useMemo, useRef, useState } from "react";
import { Datagrid, type ColumnConfig, type DatagridHandle, type GridOptions } from "@datagrid/react-ui";
import { createTasks, type Task } from "../data";
const rows = createTasks(150);
const groups = ["project", "status"];
const columns: Array<ColumnConfig<Task>> = [
{ field: "project", text: "Project", groupable: true },
{ field: "status", text: "Status", groupable: true },
{ field: "title", text: "Task" },
{ field: "assignee", text: "Assignee" },
{ field: "estimate", text: "Estimate (h)", type: "number", aggFunc: "sum" },
];
export default function GroupingExpanded() {
const grid = useRef<DatagridHandle>(null);
const [expanded, setExpanded] = useState(1);
const [showCount, setShowCount] = useState(true);
const options = useMemo<GridOptions<Task>>(
() => ({
groupDefaultExpanded: expanded,
groupShowCount: showCount,
defaultColumn: { flex: 1 },
}),
[expanded, showCount]
);
return (
<div className="demo">
<div className="demo-toolbar">
<label>
groupDefaultExpanded{" "}
<select value={expanded} onChange={(e) => setExpanded(Number(e.target.value))}>
<option value={0}>0 – all collapsed</option>
<option value={1}>1 – first level expanded</option>
<option value={-1}>-1 – all expanded</option>
</select>
</label>
<label>
<input type="checkbox" checked={showCount} onChange={(e) => setShowCount(e.target.checked)} /> groupShowCount
</label>
<button onClick={() => grid.current?.expandAll()}>Expand all</button>
<button onClick={() => grid.current?.collapseAll()}>Collapse all</button>
</div>
{/* groupDefaultExpanded applies to new groups, the key recreates the grid */}
<Datagrid
ref={grid}
key={expanded}
columns={columns}
rows={rows}
options={options}
columnGroups={groups}
/>
</div>
);
}Aggregation
Columns with an aggFunc show aggregated values in group rows and in the total row.
aggFunc | Result |
|---|---|
"sum" | sum of all numbers |
"avg" | average of all numbers |
"min", "max" | smallest / largest value (numbers, dates, strings) |
"count" | number of rows |
"first", "last" | value of the first / last row |
| function | (values, rows) => result |
The aggregated value is formatted with the valueFormatter of the column.
The demo groups tasks by project and uses one aggregation per column. Progress uses a custom function which receives the values and the rows, so it can weight the progress by the estimate of each task. The same column can be added twice with different ids (here the due date with min and max). Switch the position of the total row with the dropdown.
Source
vue
<template>
<div class="demo">
<div class="demo-toolbar">
<label>
Total row
<select v-model="totalRow">
<option value="bottom">bottom</option>
<option value="top">top</option>
<option value="none">none</option>
</select>
</label>
</div>
<Datagrid :columns="columns" :rows="rows" :options="options" :columnGroups="groups" />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, type Task } from "./data";
const rows = createTasks(120);
const groups = ["project"];
const totalRow = ref<"bottom" | "top" | "none">("bottom");
const hours = ({ value }: { value: unknown }) => (typeof value === "number" ? `${Math.round(value)} h` : "");
const columns: Array<ColumnConfig<Task>> = [
{ field: "project", text: "Project", groupable: true, visible: false },
{ field: "title", text: "Task", width: 170 },
{ id: "tasks", field: "id", text: "Tasks (count)", type: "number", aggFunc: "count", width: 120 },
{ field: "estimate", text: "Estimate (sum)", type: "number", aggFunc: "sum", valueFormatter: hours },
{ field: "spent", text: "Spent (avg)", type: "number", aggFunc: "avg", valueFormatter: hours },
{
field: "progress",
text: "Progress (weighted)",
type: "number",
width: 150,
// custom aggregation: average progress weighted by the estimate of each task
aggFunc: (values, rows) => {
const total = rows.reduce((sum, row) => sum + row.data.estimate, 0);
const done = rows.reduce((sum, row) => sum + row.data.progress * row.data.estimate, 0);
return total ? done / total : 0;
},
valueFormatter: formatters.percent({ maximumFractionDigits: 0 }),
},
{ field: "due", text: "First due (min)", type: "date", aggFunc: "min", valueFormatter: formatters.date() },
{ id: "lastDue", field: "due", text: "Last due (max)", type: "date", aggFunc: "max", valueFormatter: formatters.date() },
];
const options = computed<GridOptions<Task>>(() => ({
totalRow: totalRow.value === "none" ? undefined : totalRow.value,
groupDefaultExpanded: 0,
defaultColumn: { sortable: true, width: 125 },
}));
</script>tsx
import { useMemo, useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createTasks, type Task } from "../data";
const rows = createTasks(120);
const groups = ["project"];
const hours = ({ value }: { value: unknown }) => (typeof value === "number" ? `${Math.round(value)} h` : "");
const columns: Array<ColumnConfig<Task>> = [
{ field: "project", text: "Project", groupable: true, visible: false },
{ field: "title", text: "Task", width: 170 },
{ id: "tasks", field: "id", text: "Tasks (count)", type: "number", aggFunc: "count", width: 120 },
{ field: "estimate", text: "Estimate (sum)", type: "number", aggFunc: "sum", valueFormatter: hours },
{ field: "spent", text: "Spent (avg)", type: "number", aggFunc: "avg", valueFormatter: hours },
{
field: "progress",
text: "Progress (weighted)",
type: "number",
width: 150,
// custom aggregation: average progress weighted by the estimate of each task
aggFunc: (values, rows) => {
const total = rows.reduce((sum, row) => sum + row.data.estimate, 0);
const done = rows.reduce((sum, row) => sum + row.data.progress * row.data.estimate, 0);
return total ? done / total : 0;
},
valueFormatter: formatters.percent({ maximumFractionDigits: 0 }),
},
{ field: "due", text: "First due (min)", type: "date", aggFunc: "min", valueFormatter: formatters.date() },
{ id: "lastDue", field: "due", text: "Last due (max)", type: "date", aggFunc: "max", valueFormatter: formatters.date() },
];
export default function GroupingAggregation() {
const [totalRow, setTotalRow] = useState<"bottom" | "top" | "none">("bottom");
const options = useMemo<GridOptions<Task>>(
() => ({
totalRow: totalRow === "none" ? undefined : totalRow,
groupDefaultExpanded: 0,
defaultColumn: { sortable: true, width: 125 },
}),
[totalRow]
);
return (
<div className="demo">
<div className="demo-toolbar">
<label>
Total row{" "}
<select value={totalRow} onChange={(e) => setTotalRow(e.target.value as "bottom" | "top" | "none")}>
<option value="bottom">bottom</option>
<option value="top">top</option>
<option value="none">none</option>
</select>
</label>
</div>
<Datagrid columns={columns} rows={rows} options={options} columnGroups={groups} />
</div>
);
}ts
{
field: "remote",
aggFunc: (values) => values.filter(Boolean).length / values.length,
valueFormatter: ({ value }) => `${Math.round(value * 100)} %`,
}Groups can be sorted by aggregated values: sorting an aggregated column sorts the groups by their aggregated value.
Total row
totalRow: "bottom" or "top" adds a pinned row with the aggregations over all filtered rows.
Group footers
groupFooter: true adds a footer row below the rows of each expanded group, like the subtotals of a report. It shows the same aggregated values as the group row and the label "Total" with the group key (translation key group_total). A collapsed group has no footer, its group row shows the aggregations.
Footer rows scroll with the rows, they are not part of the data (api.getData()) and are not selectable or editable. With nested grouping every level gets its own footer, so the last rows of the demo below show the department subtotal followed by the country total.
Sticky group rows
With stickyGroupRows: true the group rows of the rows at the top of the viewport stay visible below the header while scrolling – one row per grouping level. You always see which group the visible rows belong to. Clicking a sticky group row collapses the group and scrolls it into view.
Source
vue
<template>
<Datagrid :columns="columns" :rows="rows" :options="options" v-model:columnGroups="groups" />
</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 rows = createEmployees(300);
const groups = ref(["country", "department"]);
const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });
const columns: Array<ColumnConfig<Employee>> = [
{ field: "country", text: "Country", groupable: true, visible: false },
{ field: "department", text: "Department", groupable: true, visible: false },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "salary", text: "Salary", type: "number", aggFunc: "sum", valueFormatter: currency },
{ field: "bonus", text: "Bonus", type: "number", aggFunc: "sum", valueFormatter: currency },
];
const options: GridOptions<Employee> = {
groupDefaultExpanded: -1, // expand all levels
groupFooter: true, // sums below each group
stickyGroupRows: true, // group rows stay visible while scrolling
defaultColumn: { flex: 1 },
};
</script>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(300);
const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });
const columns: Array<ColumnConfig<Employee>> = [
{ field: "country", text: "Country", groupable: true, visible: false },
{ field: "department", text: "Department", groupable: true, visible: false },
{ field: "firstName", text: "First name" },
{ field: "lastName", text: "Last name" },
{ field: "salary", text: "Salary", type: "number", aggFunc: "sum", valueFormatter: currency },
{ field: "bonus", text: "Bonus", type: "number", aggFunc: "sum", valueFormatter: currency },
];
const options: GridOptions<Employee> = {
groupDefaultExpanded: -1, // expand all levels
groupFooter: true, // sums below each group
stickyGroupRows: true, // group rows stay visible while scrolling
defaultColumn: { flex: 1 },
};
export default function GroupFooterSticky() {
const [groups, setGroups] = useState(["country", "department"]);
return (
<Datagrid columns={columns} rows={rows} options={options} columnGroups={groups} onColumnGroupsChange={setGroups} />
);
}| Option | Default | Description |
|---|---|---|
groupFooter | false | footer row with the aggregations below each expanded group |
stickyGroupRows | false | group rows of the top visible rows stay at the top of the grid |
Customizing group rows
The group slot replaces the content of group rows:
vue
<Datagrid ...>
<template #group="{ group, key, text, count, column }">
<strong>{{ text }}</strong> – {{ count }} employees
</template>
</Datagrid>renderGroup replaces the content of group rows:
tsx
<Datagrid
renderGroup={({ group, key, text, count, column }) => (
<>
<strong>{text}</strong> – {count} employees
</>
)}
...
/>Options: groupShowCount: false hides the number of rows.
Use a custom group row when the group key needs more context than a text, e.g. a status badge and a summary of the rows. group.rows contains all data rows of the group (including nested groups), so any summary can be calculated. For values which are shown in the columns, prefer aggFunc.
Source
vue
<template>
<Datagrid :columns="columns" :rows="rows" :options="options" :columnGroups="groups">
<!-- content of the group rows -->
<template #group="{ group, key, count }">
<span class="gcr-badge" :class="`gcr-badge--${String(key).replace(' ', '-')}`">{{ key }}</span>
<span class="gcr-info">{{ count }} tasks · {{ summary(group).hours }} h estimated</span>
<span v-if="summary(group).overdue" class="gcr-overdue">{{ summary(group).overdue }} overdue</span>
</template>
</Datagrid>
</template>
<script setup lang="ts">
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type Group } from "@datagrid/vue-ui";
import { createTasks, TODAY, type Task } from "./data";
const rows = createTasks(80);
const groups = ["status"];
// group.rows contains all data rows of the group
function summary(group: Group) {
const tasks = group.rows.map((row) => row.data as Task);
return {
hours: tasks.reduce((sum, task) => sum + task.estimate, 0),
overdue: tasks.filter((task) => !task.done && task.due < TODAY).length,
};
}
const columns: Array<ColumnConfig<Task>> = [
{ field: "status", text: "Status", groupable: true, visible: false },
{ field: "title", text: "Task", flex: 2 },
{ field: "assignee", text: "Assignee", flex: 1 },
{ field: "estimate", text: "Estimate (h)", type: "number", width: 120 },
{ field: "due", text: "Due", type: "date", valueFormatter: formatters.date(), width: 140 },
];
const options: GridOptions<Task> = {
groupDefaultExpanded: 1,
};
</script>
<style>
.gcr-badge {
padding: 1px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
text-transform: capitalize;
color: white;
background: #6e7781;
}
.gcr-badge--in-progress {
background: #0969da;
}
.gcr-badge--review {
background: #8250df;
}
.gcr-badge--done {
background: #1a7f37;
}
.gcr-info {
margin-left: 8px;
color: var(--ut-muted-color);
}
.gcr-overdue {
margin-left: 8px;
color: #cf222e;
font-weight: 600;
}
</style>tsx
import {
Datagrid,
formatters,
type ColumnConfig,
type DatagridProps,
type GridOptions,
type Group,
} from "@datagrid/react-ui";
import { createTasks, TODAY, type Task } from "../data";
const rows = createTasks(80);
const groups = ["status"];
// group.rows contains all data rows of the group
function summary(group: Group) {
const tasks = group.rows.map((row) => row.data as Task);
return {
hours: tasks.reduce((sum, task) => sum + task.estimate, 0),
overdue: tasks.filter((task) => !task.done && task.due < TODAY).length,
};
}
const columns: Array<ColumnConfig<Task>> = [
{ field: "status", text: "Status", groupable: true, visible: false },
{ field: "title", text: "Task", flex: 2 },
{ field: "assignee", text: "Assignee", flex: 1 },
{ field: "estimate", text: "Estimate (h)", type: "number", width: 120 },
{ field: "due", text: "Due", type: "date", valueFormatter: formatters.date(), width: 140 },
];
const options: GridOptions<Task> = {
groupDefaultExpanded: 1,
};
// content of the group rows
const renderGroup: DatagridProps<Task>["renderGroup"] = ({ group, key, count }) => {
const { hours, overdue } = summary(group);
return (
<>
<span className={`gcr-badge gcr-badge--${String(key).replace(" ", "-")}`}>{String(key)}</span>
<span className="gcr-info">
{count} tasks · {hours} h estimated
</span>
{overdue > 0 && <span className="gcr-overdue">{overdue} overdue</span>}
</>
);
};
export default function GroupingCustomRow() {
return (
<>
<Datagrid columns={columns} rows={rows} options={options} columnGroups={groups} renderGroup={renderGroup} />
<style>{`
.gcr-badge {
padding: 1px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
text-transform: capitalize;
color: white;
background: #6e7781;
}
.gcr-badge--in-progress {
background: #0969da;
}
.gcr-badge--review {
background: #8250df;
}
.gcr-badge--done {
background: #1a7f37;
}
.gcr-info {
margin-left: 8px;
color: var(--ut-muted-color);
}
.gcr-overdue {
margin-left: 8px;
color: #cf222e;
font-weight: 600;
}
`}</style>
</>
);
}Keyboard
Group rows can be focused with ↑ / ↓. Enter or Space toggles a group, → expands and ← collapses it (or moves to the parent group).