Skip to content

Stat Card

ComponentMantine ≥ 9

Metric tile with label, value, optional icon and a trend row.

Install

One command copies the item's source into your project, where you own and edit it.

Terminal window
npx manteen add @house/stat-card

Dependencies

  • @mantine/core@^9npm
  • @tabler/icons-react@^3npm

Preview

Usage

A copy-ready example published by the registry author. Imports use the aliases Manteen configures at init.

stat-card.usage.tsx
import { SimpleGrid } from "@mantine/core";
import { IconCoin, IconUsers } from "@tabler/icons-react";
import { StatCard } from "@/components/ui/stat-card";
export function RevenueOverview() {
return (
<SimpleGrid cols={2}>
<StatCard label="Revenue" value="$48,200" diff={12.4} icon={<IconCoin size={20} />} />
<StatCard label="Active users" value="3,128" diff={-4.1} icon={<IconUsers size={20} />} />
</SimpleGrid>
);
}

Props

Author-documented props, carried verbatim from the registry item.

PropTypeDefaultDescription
label*stringRenders the uppercase, muted caption identifying what the stat measures.
value*ReactNodeRenders the large, bold headline figure for the stat.
diffnumberPercentage change vs. the previous period; renders a colored trend row with an up/down arrow, and is hidden entirely when omitted.
iconReactNodeRenders inside a light-variant ThemeIcon at the top-right of the card; the icon slot is omitted entirely when not provided.
...othersBoxProps & ElementProps<"div">All other props are forwarded to the root Paper, after its own withBorder/p="md"/radius="md", so both can be overridden. Mantine Styles API props (classNames, styles, unstyled) are also accepted.

* required

Styling

StatCard exposes these parts through the public classNames/styles interface:

  • root
  • label
  • value
  • icon
  • diff

Author-declared; Manteen reports the declaration but does not verify each selector is wired.

Source

2 installable files ship with this item.

registry/ui/stat-card.tsxcomponent · 2.7 kB · tsx
import {
type BoxProps,
type ElementProps,
type Factory,
factory,
Group,
Paper,
type StylesApiProps,
Text,
ThemeIcon,
useProps,
useStyles,
} from "@mantine/core";
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
import type { ReactNode } from "react";
import classes from "./stat-card.module.css";
export type StatCardStylesNames = "root" | "label" | "value" | "icon" | "diff";
export interface StatCardProps
extends BoxProps,
StylesApiProps<StatCardFactory>,
ElementProps<"div"> {
label: string;
value: ReactNode;
/** Percentage change vs. the previous period. Omit to hide the trend row. */
diff?: number;
icon?: ReactNode;
}
export type StatCardFactory = Factory<{
props: StatCardProps;
ref: HTMLDivElement;
stylesNames: StatCardStylesNames;
}>;
export const StatCard = factory<StatCardFactory>((_props) => {
const props = useProps("StatCard", null, _props);
const {
classNames,
className,
style,
styles,
unstyled,
vars,
attributes,
ref,
label,
value,
diff,
icon,
...others
} = props;
const getStyles = useStyles<StatCardFactory>({
name: "StatCard",
classes,
props,
className,
style,
classNames,
styles,
unstyled,
attributes,
vars,
});
const positive = (diff ?? 0) >= 0;
return (
<Paper
ref={ref}
withBorder
p="md"
radius="md"
unstyled={unstyled}
{...getStyles("root")}
{...others}
>
<Group justify="space-between" wrap="nowrap">
<div>
<Text {...getStyles("label")}>{label}</Text>
<Text {...getStyles("value")}>{value}</Text>
</div>
{icon && (
<ThemeIcon variant="light" size={38} radius="md" {...getStyles("icon")}>
{icon}
</ThemeIcon>
)}
</Group>
{diff !== undefined && (
<Group gap={4} mt="xs" {...getStyles("diff")}>
<ThemeIcon variant="transparent" size="sm" c={positive ? "teal" : "red"}>
{positive ? <IconTrendingUp size={16} /> : <IconTrendingDown size={16} />}
</ThemeIcon>
<Text size="sm" c={positive ? "teal" : "red"} fw={500}>
{positive ? "+" : ""}
{diff}%
</Text>
<Text size="sm" c="dimmed">
vs last period
</Text>
</Group>
)}
</Paper>
);
});
StatCard.classes = classes;
StatCard.displayName = "StatCard";
export namespace StatCard {
export type Props = StatCardProps;
export type StylesNames = StatCardStylesNames;
export type Factory = StatCardFactory;
}
registry/ui/stat-card.module.cssstyle · 1.8 kB · css
.root {
/*
* Structural anchor for the Styles API root selector. The visual
* defaults (withBorder, padding, radius) stay as component props on
* Paper, not CSS, so a caller's passthrough props can still override
* them exactly as they could pre-conversion.
*/
}
.label {
/*
* Matches Text's own size="xs" resolution (both --text-fz and --text-lh
* derive from `size` in Text's varsResolver), so the default render is
* unchanged now that the value moved from a prop to this class.
*/
font-size: var(--mantine-font-size-xs);
line-height: var(--mantine-line-height-xs);
color: var(--mantine-color-dimmed);
text-transform: uppercase;
font-weight: 600;
}
.value {
/*
* fz={28} and mt={4} previously resolved through Mantine's rem()
* converter (value/16 -> rem, scaled by --mantine-scale). Reproduced
* here verbatim so the default render doesn't drift under a themed
* `scale` override. lh={1.2} was a unitless number, passed through
* as-is by lineHeightResolver, so it needs no such conversion.
*/
font-size: calc(1.75rem * var(--mantine-scale));
font-weight: 700;
line-height: 1.2;
margin-top: calc(0.25rem * var(--mantine-scale));
}
.icon {
/*
* Structural selector for the optional ThemeIcon slot. Its visual
* defaults (variant, size, radius) stay as component props so a
* caller's passthrough props can still override them.
*/
}
.diff {
/*
* Structural selector for the trend row (arrow ThemeIcon + diff text +
* "vs last period" caption share this one selector, per the Styles API
* conversion guide's rule that conditional/repeated parts of the same
* kind get one name rather than one each). Colors stay dynamic props
* (positive/negative) rather than CSS since they depend on the diff sign.
*/
}