On this page
Import
import {
AnimatedNumber,
Badge,
Button,
Card,
Column,
Grid,
Row,
Surface,
Text,
} from '@envsemble/envui';
Usage
A key figure is a Column of three Texts.
- The label, with
variant="body-sm"andemphasis="muted". - The figure, with
variant="headline"andfontVariantNumeric: 'tabular-nums', so it keeps its width. - A Row with the trend Badge and a muted description.
The Badge takes the change in words and an icon, such as TrendingUp or TrendingFlat, and its tone says whether it is good news: success, danger, or neutral for no change. A figure that changes while people look at it is an AnimatedNumber inside the figure's Text.
import { useState, type ReactNode } from 'react';
import {
AnimatedNumber,
Badge,
Button,
Column,
Grid,
Row,
Surface,
Text,
type BadgeTone,
} from '@envsemble/envui';
import { IconAdd } from '@envsemble/envui/icons/Add';
import { IconTrendingFlat } from '@envsemble/envui/icons/TrendingFlat';
import { IconTrendingUp } from '@envsemble/envui/icons/TrendingUp';
/** A labelled figure, with how it changed in words and over what period. */
function KeyFigure({
label,
value,
trend,
description,
}: {
label: string;
value: ReactNode;
trend?: { icon: ReactNode; label: string; tone: BadgeTone };
description?: string;
}) {
return (
<Column gap={1}>
<Text variant="body-sm" emphasis="muted">
{label}
</Text>
{/* Tabular digits, so the figure keeps its width as it changes. */}
<Text variant="headline" style={{ fontVariantNumeric: 'tabular-nums' }}>
{value}
</Text>
{(trend || description) && (
<Row wrap align="center" gap={2}>
{trend && (
<Badge tone={trend.tone} icon={trend.icon}>
{trend.label}
</Badge>
)}
{description && (
<Text render={<span />} variant="body-sm" emphasis="muted">
{description}
</Text>
)}
</Row>
)}
</Column>
);
}
/** Active projects a month ago, which the trend compares with. */
const lastMonth = 114;
export default function ProjectHealth() {
const [projects, setProjects] = useState(128);
const change = Math.round(((projects - lastMonth) / lastMonth) * 100);
return (
<Column gap={4}>
{/* As many even columns as fit, each at least 11 rem wide; named, so it reads as a group. */}
<Surface
role="group"
aria-label="Project health"
render={
<Grid
name="key-figures"
columns="repeat(auto-fit, minmax(min(100%, 11rem), 1fr))"
gap={5}
/>
}
>
<KeyFigure
label="Active projects"
value={<AnimatedNumber value={projects} />}
trend={{ icon: <IconTrendingUp />, label: `+${change}%`, tone: 'success' }}
description="Last 30 days"
/>
<KeyFigure
label="Tasks done"
value="1,284"
trend={{ icon: <IconTrendingUp />, label: '+86', tone: 'success' }}
description="This week"
/>
<KeyFigure
label="Lead time"
value="2.5 days"
trend={{ icon: <IconTrendingFlat />, label: 'No change', tone: 'neutral' }}
description="Median"
/>
</Surface>
<Row wrap align="center" gap={4}>
<Button
variant="secondary"
startIcon={<IconAdd />}
onClick={() => setProjects((count) => count + 1)}
>
Add a project
</Button>
</Row>
</Column>
);
}On a pane or in a card
On a pane, the group's Surface takes background="muted", inside the pane's own background="default" Surface with its heading. A single figure sits in a Card.
Composition
The group is a Surface with role="group" and an aria-label, rendering a Grid through render. The Grid's columns are repeat(auto-fit, minmax(min(100%, 11rem), 1fr)): as many even columns as fit, each at least 11 rem wide, and one column below that.
<Surface
role="group"
aria-label="Project health"
render={<Grid columns="repeat(auto-fit, minmax(min(100%, 11rem), 1fr))" gap={5} />}
>
{/* One Column per figure */}
</Surface>
Accessibility
You provide
role="group"on the Surface that holds the figures, named byaria-labeloraria-labelledby.- A trend in words that say which way, such as +12% or 2 fewer, never in color or an arrow alone.
- Tabular digits on the figure,
font-variant-numeric: tabular-nums, so it keeps its width as it changes; AnimatedNumber uses them. - A heading level for the group's own heading, where it has one, that fits the page; the figures themselves are not headings.
- Figures formatted in the reader's language.
envUI provides
- The Badge's icon hidden from assistive technology, so its words are what is read.
Changelog
- 1.0.0
Key figures are a pattern built from Surface, Grid, Text and Badge, in place of the Stat component, with Stat.Root and Stat.Group.