GrStatistic
Highlights a key metric with a caption, formatting and a trend line.
Overview
When to take it
- one metric is the main one — revenue, the number of orders, conversion: a large figure is read first;
- a trend is needed beside it —
trendwith an arrow and the label “+12 % against last week”; - the number is long — digit grouping and
precisionby locale, with no manual formatting; - the value changes before your eyes —
animatebrings the figure to the new value rather than replacing it; - the tile is clickable —
href/clickablelead into a section with the details.
When to take something else
| Need | Take |
|---|---|
| There are several metrics and they are compared | GrChartBar |
| The course over time matters | GrChartLine / GrSparkline |
| A share of a whole is shown | GrProgressCircle |
| The value is a status rather than a number | GrBadge |
| The quantity stands inside a line of text rather than as a tile | GrDelta |
The component is presentational: it does not load the data and does not compute the delta — it only formats and presents. The trend arrives ready, because “compared to what” is known by the application rather than by the tile.
Variants every tone and view, rendered by the real component
tone
primaryneutralsuccesswarningdangerinfoslateazure
Sizes
xssmmdlg
States
normalloading
The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrStatistic title="Revenue" :value="1284500" />Examples 4 each with a live demo and its code
A number with a name
The value is formatted by the locale, the sign and the unit live in
prefixandsuffix, and the title is part of the component — a number without a name is not a statistic.- Revenue
- €92,350
- Active customers
- 18,342
- Conversion
- 3.1%
Growth next to the value
trendis a direction —up,down,flat— andtrend-textis the phrase under the number; the change itself comes from the metric, the component does not compute it. A rise is good news by default. For churn a fall is:trend-polarity="negative-good"paints the trend line green and leaves the value neutral.- Recurring revenue
- €48,920
Increase+8.2% this month- Trial conversion
- 18.6%
Decrease−1.2 pp this month- Monthly churn
- 2.1%
Decrease−0.4 pp this month- Seats per account
- 4.2
No changeNo changeWaiting and a click through
loadingholds the place of the number instead of showing a zero, andhrefturns the whole tile into a link to the page where the number can be taken apart.Support desk
A dashboard of several figures
Figures are read together rather than one at a time: four tiles for the period and the daily revenue under them. Switch the period —
animatecounts from the previous value, so the change is visible instead of a new number appearing out of nowhere.Store overview
- Revenue
- €21,480
Increase+12.4% vs previous- Orders
- 486
Increase+6.1% vs previous- Conversion
- 3.4%
Decrease−0.3 pp vs previous- Average basket
- €44.20
No changeNo changeDaily revenue
Thousands of euros
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone |
| undefined | The tone of the value; overridden pointwise by `--gr-statistic-value-color`. |
title | string | undefined | undefined | The label above the value. |
icon | string | Component | undefined | undefined | An icon to the left of the block: a Vue component or an icon class (`'i-lucide-users'` — the application sets up the rule for the class: in the engine factory or through a provider, see the “Icons” section of `docs/installation.md`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The type size of the value, and with it of the title, the additions and the dynamics line. Unset — from `GrConfigProvider`, otherwise `md`. |
loading | boolean | undefined | false | The loading state: a placeholder instead of the value. |
animate | boolean | undefined | false | Run through the numbers on appearing and on a change of the value. A non-numeric value is put in at once, and under `prefers-reduced-motion: reduce` so is any other. |
as | string | Component | undefined | undefined | A root tag of your own: a link component (`RouterLink`, Inertia’s `Link`) or a semantic tag of the page (`section`, `article`). Stronger than `href`. A non-interactive tag is only a replacement for `div`: no highlight, no focus ring. |
href | string | undefined | undefined | A metric as a link: a move to the details. |
clickable | boolean | undefined | false | A metric as a button: the whole tile is interactive. |
locale | string | undefined | undefined | The BCP-47 locale of the formatting. Unset — it is taken from the i18n adapter; with no adapter either, the manual separators work. |
precision | number | undefined | undefined | The number of digits after the decimal point. |
prefix | string | undefined | undefined | An addition before the value (a currency, a sign). |
suffix | string | undefined | undefined | An addition after the value (a unit of measurement, `%`). |
polarity | GrDeltaPolarity | undefined | undefined | What counts as good for a **signed value** — a balance, a gain, a difference: the tone is derived from the sign of the value itself. For revenue growth is a success, for the cost price and churn it is the other way round. An explicit `tone` is stronger: the derived tone is a default rather than a dictate. An absolute figure (churn of 2.4 %, response time) carries no sign — for it the polarity is given to the dynamics, `trendPolarity`, and the value stays neutral. |
decimalSeparator | string | undefined | undefined | The decimal separator. Stronger than the locale; without it and without a locale — a full stop. |
groupSeparator | string | undefined | undefined | The digit-group separator. Stronger than the locale; without it and without a locale — a narrow space. |
trendPolarity | GrDeltaPolarity | undefined | undefined | What counts as good for the **dynamics**: `negative-good` paints a rise in `danger` and a fall in `success`, `none` leaves it without a tone. When unset, `polarity` is taken, and without it a rise is good. |
trend | GrStatisticTrend | undefined | undefined | The direction of the dynamics — it sets the icon of the line under the value and, with a polarity, its colour too. |
trendText | string | undefined | undefined | The text of the dynamics (`+12.5% against last week`, for instance). |
animateDuration | number | undefined | 600 | The duration of the run-through in milliseconds. Not a token: the `--gr-duration-*` scale ends at 300 ms and describes a change of state, whereas running through numbers is a different genre, and its number lives where the tween lives. |
valuerequired | string | number | — | The value of the metric. A non-numeric string is printed as it is. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The value instead of the `value` prop. |
icon | any | An icon before the label. |
title | any | The label of the metric instead of the `title` prop. |
prefix | any | An addition before the value: a currency sign, an arrow. |
suffix | any | An addition after the value: a unit of measurement, a per cent. |
trend | any | The dynamics under the value instead of the built-in `GrDelta`. |
Events
| Event | Signature | Description |
|---|---|---|
click | (event: "click", event: MouseEvent): void | — |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrStatisticFormatOptions, GrStatisticProps, GrStatisticEmits } from '@feugene/granularity/components/GrStatistic'Filedist/types/components/GrStatistic/config.d.ts
const grStatisticConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrStatistic">;Filedist/types/components/GrStatistic/formatStatisticValue.d.ts
type GrStatisticFormatOptions = {
precision?: number;
groupSeparator?: string;
decimalSeparator?: string;
locale?: string;
};function formatStatisticValue(value: number | string, options?: GrStatisticFormatOptions): string;Filedist/types/components/GrStatistic/GrStatistic.vue.d.ts
interface GrStatisticProps {
value: number | string;
title?: string;
precision?: number;
groupSeparator?: string;
decimalSeparator?: string;
locale?: string;
prefix?: string;
suffix?: string;
icon?: string | Component;
size?: GrStatisticSize;
tone?: GrStatisticTone;
polarity?: GrDeltaPolarity;
trendPolarity?: GrDeltaPolarity;
trend?: GrStatisticTrend;
trendText?: string;
loading?: boolean;
animate?: boolean;
animateDuration?: number;
as?: string | Component;
href?: string;
clickable?: boolean;
}interface GrStatisticEmits {
(e: 'click', event: MouseEvent): void;
}Filedist/types/components/GrStatistic/grStatisticStyles.d.ts
type GrStatisticSize = GrComponentSize;type GrStatisticTone = GrTone;type GrStatisticTrend = 'up' | 'down' | 'flat';Filedist/types/components/GrStatistic/safelist.d.ts
const grStatisticSafelist: string[];Filedist/types/components/GrStatistic/defaults.d.ts
interface GrStatisticConfigurableProps {
size: GrStatisticSize;
polarity: GrDeltaPolarity;
trendPolarity: GrDeltaPolarity;
}Usage from the library prose
Formatting by locale
<GrStatistic title="Revenue" :value="1234567.5" :precision="2" prefix="₽" />The separators come from the locale through Intl.NumberFormat, and the locale from the i18n
adapter of the package. Nothing has to be configured: ru will give 1 234 567,50, and de-DE
1.234.567,50.
The resolution order: groupSeparator/decimalSeparator → the locale prop → the locale of the
adapter → the built-in default (a narrow space and a dot). An explicit separator replaces only
its own part; the rest of the rules of the locale — grouping by three or by the Indian scheme, the
minus sign — the component keeps.
A non-numeric value is printed as it is: “2 h 15 min” and ”—” need no workaround props. The
formatting lives in a pure formatStatisticValue function of its own and is available from the
package.
A mandatory value that did not arrive is drawn as a dash — the same sign of emptiness with which
GrDelta shows a missing quantity — and is explained with a warning in dev mode. A tile with the
word undefined looks like data rather than like an error, so that variant is excluded.
The trend
<GrStatistic :value="482" trend="up" trend-text="+12.5 % against last week" />trend sets the colour and the icon of the line, and trendText the text itself. The direction
icons are built in; the icon prop to the left of the block accepts a Vue component or an icon
class from your build (see “Icons”). The direction is additionally
announced with a hidden label (“Growth”, “Decline”, “No change”): the icon is decorative, and
“+12.5 %” without context does not tell growth from decline — the colour is not visible to a screen
reader either. The label stays with a #trend slot of your own as well.
The affixes are drawn by GrValue
<GrStatistic title="Average order" :value="14.99" :precision="2" prefix="$" suffix="per order" />The tile does not draw the record of the quantity — the prefix, the number, the suffix — itself:
that is done by the GrValue primitive, shared with GrDelta. The defaults come
from there as well: on the left the affix is set like a number ($14.99), on the right it is dimmed
and smaller (42 %).
The tile decides only the tone and the type size, and puts them on the container of the quantity — the affixes inherit both. That is why a negative sum turns red as a whole, together with the currency sign, rather than the number apart from it.
What an affix is — a currency, a unit of measurement or a note — is decided by nobody: the styling
of both is configured with the --gr-value-* tokens. That is also how a currency on the right
(1 284 500 ₽) is obtained, for which the default of the suffix does not fit — the recipe is on
the page of the primitive.
The tone by sign and the trend line are different signals
<GrStatistic title="Margin" :value="-1240" prefix="₽" polarity="positive-good" />polarity is for a signed value: a balance, an increase, a difference from the plan. It
colours the value by the sign of the quantity itself: positive-good for margin, negative-good
for overspending, none when the sign says nothing about quality. Zero is neutral with any
polarity, and a non-numeric value (“2 h 15 min”, ”—”) has no sign and gets no tone.
trend sets the line under the value: the direction with an icon and a hidden label, and the
colour with the polarity rather than the direction. Growing churn is bad news, falling overdue debt
is good news; the “up” arrow is green only where growth really is good. The tone is computed by the
same deltaTone: negative-good colours up as danger, down as success, none and flat
get no tone.
<GrStatistic title="Churn" :value="2.4" suffix="%" trend="up" trend-text="+0.6 pp" trend-polarity="negative-good" />An absolute metric — a churn of 2.4 %, a response time, the number of overdue invoices — carries no
sign: there is nothing to colour the number itself for. Such a metric gets its polarity on the
trend, trendPolarity, and the value stays neutral while the line ”↑ +0.6 pp” turns red. Without
trendPolarity the trend takes polarity; with neither set, growth is good, as with revenue.
An explicit tone is stronger than polarity: an inferred tone is a default rather than a
dictate. The same choice of tone is available as a separate deltaTone function — see
GrDelta, where it lives.
The counter animation
<GrStatistic title="Revenue" :value="revenue" animate :animate-duration="900" />animate runs through the numbers when the tile appears (from zero) and on every change of the
value — from the previous number rather than from zero: a run from zero on every update of a
dashboard would read as a reset of the data.
The duration is set by animateDuration in milliseconds (600 by default). It is deliberately not
set with a token: the --gr-duration-* scale ends at 300 ms and describes a change of state — a
colour, a border, the appearance of a layer. A run through numbers is a different genre, and its
number lives where the tween does — in JS.
Only numbers are run through: “2 h 15 min” and ”—” are set at once. The width of the line does not
jerk — the value is set in tabular-nums.
The component reads prefers-reduced-motion itself. The global clamp in base.css holds CSS
animations and transitions but not a JS tween — see ../motion.md. Under reduce
the value is set instantly, with not a single intermediate frame.
A screen reader hears the final value. While the run is going on, the visible number is marked
aria-hidden, and a visually hidden node with the result lives beside it: “1,284,500” on the screen
and “743,210” in the ears is not noise but wrong data.
The step into the details
<GrStatistic title="Orders" :value="1284" href="/orders" />
<GrStatistic title="Orders" :value="1284" clickable @click="drill" />
<GrStatistic title="Orders" :value="1284" :as="RouterLink" :to="{ name: 'orders' }" />The same device as in GrCard and GrListItem: href gives a link, clickable a
button, as a tag or a router component of your own (stronger than href). An interactive tile
gets a cursor, a highlight and a focus ring; it has no fill of its own — a metric usually lies in a
card already, and a second surface would argue with it.
There is no separate ariaLabel: the accessible name is assembled from the content, and the label
of the metric is its name.
Loading
loading replaces the value with a skeleton of the same height, so that the block does not jump;
the area is marked role="status" and contains a hidden loading text.
Styling
| Point | What it sets |
|---|---|
size (xs…lg) | the ladders of the type sizes of the label, the value, the affixes and the trend; read from GrConfigProvider |
tone | the colour of the value; the tones come from the -text tokens — a saturated tone as text does not pass by contrast |
polarity | infers tone from the sign of a signed value itself; read from GrConfigProvider |
trendPolarity | what counts as good for the trend; when not set — polarity, otherwise growth is good; read from GrConfigProvider |
--gr-statistic-value-color | the colour of the value pointwise, stronger than tone |
--gr-statistic-title-color | the colour of the label |
The #icon, #title, #prefix, #suffix and #trend slots and the default slot (instead of the
formatted value) replace the corresponding parts with markup.
The markup: the label and the value are a pair
The label and the value are output as <dl> → <dt> → <dd>: that is a “term — value” pair rather
than two neighbouring blocks a screen reader simply reads in a row. The <dl> appears only together
with the label — a definition list without a <dt> would be the same incoherence, only with a claim
to semantics.
The trend line stays outside the <dl>: only dt/dd groups are allowed inside a definition
list. Its place in the DOM is unchanged, and the layout does not change. The margins of the <dl>
and the <dd> are zeroed with a class — the preflight of the package resets them for body alone.
All of the data attributes (data-gr-statistic-title, data-gr-statistic-value, …) are kept: the
consumer’s styles written against them keep working.
Props in this example