Game UI
Meter
A HUD bar — health, fuel, progress — that glows and blinks low.
stablev0.2.0 Chromium 103 ui-scale safe
Install
Copies the file into
components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.bunx shadcn@3 add @fastrp/meterExamples
Default
Can72
Zırh60
Enerji45
Susuzluk14
Yakıt31 L
API
<Meter />A HUD bar: health, stamina, fuel, a skill's progress. A thin pill that glows in its colour, optionally cut into cells, blinking when it runs low.
| Prop | Type | Default | Notes |
|---|---|---|---|
| value* | number | — | |
| icon | ReactNode | — | |
| label | ReactNode | — | Over the bar, left: "CAN", "DAYANIKLILIK". |
| max | number | 100 | |
| segments | number | — | Split the bar into this many cells, like a HUD's armour. |
| size | "sm" | "md" | md | |
| tone | "primary" | "danger" | "success" | "warning" | "pink" | "teal" | "purple" | "cyan" | primary | |
| valueLabel | ReactNode | — | Over the bar, right; the rounded percentage when left out. `null` hides it. |
| warnBelow | number | — | Under this share (0–1), the fill blinks: low health, low fuel. |
Dependencies
npm
- None
Registry
- @fastrp/utils
Source
components/ds/meter.tsxShowHide
import type * as React from 'react';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
/** Fill and glow per tone — written out so Tailwind finds every class. */
const TONE = {
primary: 'from-primary/70 to-primary shadow-[0_0_0.5rem_hsl(var(--primary)/0.55)]',
success: 'from-success/70 to-success shadow-[0_0_0.5rem_hsl(var(--success)/0.55)]',
danger: 'from-danger/70 to-danger shadow-[0_0_0.5rem_hsl(var(--danger)/0.55)]',
warning: 'from-warning/70 to-warning shadow-[0_0_0.5rem_hsl(var(--warning)/0.55)]',
pink: 'from-pink/70 to-pink shadow-[0_0_0.5rem_hsl(var(--pink)/0.55)]',
teal: 'from-teal/70 to-teal shadow-[0_0_0.5rem_hsl(var(--teal)/0.55)]',
purple: 'from-purple/70 to-purple shadow-[0_0_0.5rem_hsl(var(--purple)/0.55)]',
cyan: 'from-cyan/70 to-cyan shadow-[0_0_0.5rem_hsl(var(--cyan)/0.55)]',
} as const;
export type MeterProps = Omit<React.ComponentProps<'div'>, 'children'> & {
value: number;
max?: number;
tone?: keyof typeof TONE;
/** Over the bar, left: "CAN", "DAYANIKLILIK". */
label?: React.ReactNode;
/** Over the bar, right; the rounded percentage when left out. `null` hides it. */
valueLabel?: React.ReactNode | null;
icon?: React.ReactNode;
/** Split the bar into this many cells, like a HUD's armour. */
segments?: number;
/** Under this share (0–1), the fill blinks: low health, low fuel. */
warnBelow?: number;
size?: 'sm' | 'md';
};
/**
* A HUD bar: health, stamina, fuel, a skill's progress. A thin pill that glows in its colour,
* optionally cut into cells, blinking when it runs low.
*/
function Meter({
value,
max = 100,
tone = 'primary',
label,
valueLabel,
icon,
segments,
warnBelow,
size = 'md',
className,
...props
}: MeterProps) {
const share = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0;
const low = warnBelow !== undefined && share < warnBelow;
const step = segments ? 100 / segments : 0;
return (
// biome-ignore lint/a11y/useSemanticElements: a native <meter> cannot hold the label and the drawn fill
<div
data-slot="meter"
role="meter"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
aria-label={typeof label === 'string' ? label : undefined}
className={cn('w-full', className)}
{...props}
>
{(label || icon || valueLabel !== null) && (
<div className="mb-1.5 flex items-center justify-between gap-3 font-display text-label uppercase">
<span className="flex items-center gap-1.5 text-foreground/80 [&_svg]:size-3.5">
{icon}
{label}
</span>
{valueLabel !== null && (
<span className="tabular-nums text-foreground">
{valueLabel ?? Math.round(share * 100)}
</span>
)}
</div>
)}
<div
className={cn(
'relative overflow-hidden rounded-full bg-foreground/15',
size === 'sm' ? 'h-1' : 'h-1.5',
)}
>
<div
className={cn(
'h-full rounded-full bg-gradient-to-r transition-[width] duration-slow ease-brand',
TONE[tone],
low && 'animate-blink',
)}
style={{ width: `${share * 100}%` }}
/>
{step > 0 && (
<div
aria-hidden
className="absolute inset-0"
// Gaps as a share of the bar, so nothing here is in pixels.
style={{
backgroundImage: `repeating-linear-gradient(to right, transparent 0 ${step - 0.8}%, hsl(var(--ink)) ${step - 0.8}% ${step}%)`,
}}
/>
)}
</div>
</div>
);
}
export { Meter };