Stat Card
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.
npx manteen add @house/stat-cardDependencies
@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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
label* | string | — | Renders the uppercase, muted caption identifying what the stat measures. |
value* | ReactNode | — | Renders the large, bold headline figure for the stat. |
diff | number | — | Percentage change vs. the previous period; renders a colored trend row with an up/down arrow, and is hidden entirely when omitted. |
icon | ReactNode | — | Renders inside a light-variant ThemeIcon at the top-right of the card; the icon slot is omitted entirely when not provided. |
...others | BoxProps & 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:
rootlabelvalueicondiff
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. */}