Meter
Static measurement within a known range — disk usage, battery, CPU load. Uses role='meter' (not progressbar) for correct ARIA semantics. Color shifts automatically when low/high thresholds are set.
Usage
Memory usage 50%
meter.svelte
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const meterVariants = tv({
slots: {
root: 'relative flex w-full flex-col gap-1',
track: 'relative h-2 w-full overflow-hidden rounded-full bg-muted',
indicator:
'h-full rounded-full transition-[width] duration-[--motion-duration-normal] ease-[--motion-ease-out]'
},
variants: {
segment: {
default: {},
segmented: {
track: 'flex gap-px'
}
}
},
defaultVariants: {
segment: 'default'
}
});
export type MeterSegment = VariantProps<typeof meterVariants>['segment'];
export type MeterProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
value: number;
min?: number;
max?: number;
low?: number;
high?: number;
optimum?: number;
segment?: MeterSegment;
/** Human-readable label for aria-valuetext. Defaults to "X%". */
valueText?: string;
};
/**
* Threshold-to-color rules (mirrors HTML <meter> spec semantics):
*
* If neither `low` nor `high` is set → bg-primary (always).
*
* When thresholds are set, the fill color reflects "how good is this value":
* - value < low → bg-destructive (sub-optimal / danger zone)
* - low <= value <= high → bg-warning (approaching limit)
* - value > high → bg-success (within safe range)
*
* `optimum` is a reference point that doesn't shift color; it represents
* the ideal value within the [min, max] range (matching the HTML spec).
*/
export function getMeterIndicatorColor(value: number, low?: number, high?: number): string {
if (low === undefined && high === undefined) return 'bg-primary';
if (low !== undefined && value < low) return 'bg-destructive';
if (high !== undefined && value > high) return 'bg-success';
return 'bg-warning';
}
</script>
<script lang="ts">
let {
ref = $bindable(null),
class: className,
value,
min = 0,
max = 100,
low,
high,
optimum,
segment = 'default',
valueText,
...restProps
}: MeterProps = $props();
const clampedValue = $derived(Math.min(Math.max(value, min), max));
const percentage = $derived(((clampedValue - min) / (max - min)) * 100);
const resolvedValueText = $derived(valueText ?? `${Math.round(percentage)}%`);
const indicatorColor = $derived(getMeterIndicatorColor(clampedValue, low, high));
const { root, track, indicator } = $derived(meterVariants({ segment }));
</script>
<div
bind:this={ref}
data-slot="meter"
role="meter"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={clampedValue}
aria-valuetext={resolvedValueText}
class={cn(root(), className)}
{...restProps}
>
<div data-slot="meter-track" class={track()}>
<div
data-slot="meter-indicator"
class={cn(indicator(), indicatorColor)}
style="width: {percentage}%"
></div>
</div>
</div>
Thresholds
Set low and high to
enable semantic color shifts. Values below low render destructive, between them renders warning, and above high renders success.
CPU — Critical 15%
CPU — Elevated 55%
CPU — Healthy 82%
meter-thresholds.svelte
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const meterVariants = tv({
slots: {
root: 'relative flex w-full flex-col gap-1',
track: 'relative h-2 w-full overflow-hidden rounded-full bg-muted',
indicator:
'h-full rounded-full transition-[width] duration-[--motion-duration-normal] ease-[--motion-ease-out]'
},
variants: {
segment: {
default: {},
segmented: {
track: 'flex gap-px'
}
}
},
defaultVariants: {
segment: 'default'
}
});
export type MeterSegment = VariantProps<typeof meterVariants>['segment'];
export type MeterProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
value: number;
min?: number;
max?: number;
low?: number;
high?: number;
optimum?: number;
segment?: MeterSegment;
/** Human-readable label for aria-valuetext. Defaults to "X%". */
valueText?: string;
};
/**
* Threshold-to-color rules (mirrors HTML <meter> spec semantics):
*
* If neither `low` nor `high` is set → bg-primary (always).
*
* When thresholds are set, the fill color reflects "how good is this value":
* - value < low → bg-destructive (sub-optimal / danger zone)
* - low <= value <= high → bg-warning (approaching limit)
* - value > high → bg-success (within safe range)
*
* `optimum` is a reference point that doesn't shift color; it represents
* the ideal value within the [min, max] range (matching the HTML spec).
*/
export function getMeterIndicatorColor(value: number, low?: number, high?: number): string {
if (low === undefined && high === undefined) return 'bg-primary';
if (low !== undefined && value < low) return 'bg-destructive';
if (high !== undefined && value > high) return 'bg-success';
return 'bg-warning';
}
</script>
<script lang="ts">
let {
ref = $bindable(null),
class: className,
value,
min = 0,
max = 100,
low,
high,
optimum,
segment = 'default',
valueText,
...restProps
}: MeterProps = $props();
const clampedValue = $derived(Math.min(Math.max(value, min), max));
const percentage = $derived(((clampedValue - min) / (max - min)) * 100);
const resolvedValueText = $derived(valueText ?? `${Math.round(percentage)}%`);
const indicatorColor = $derived(getMeterIndicatorColor(clampedValue, low, high));
const { root, track, indicator } = $derived(meterVariants({ segment }));
</script>
<div
bind:this={ref}
data-slot="meter"
role="meter"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={clampedValue}
aria-valuetext={resolvedValueText}
class={cn(root(), className)}
{...restProps}
>
<div data-slot="meter-track" class={track()}>
<div
data-slot="meter-indicator"
class={cn(indicator(), indicatorColor)}
style="width: {percentage}%"
></div>
</div>
</div>
Storage usage
Storage 24 GB of 128 GB used
104 GB available · iCloud Drive
meter-storage.svelte
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const meterVariants = tv({
slots: {
root: 'relative flex w-full flex-col gap-1',
track: 'relative h-2 w-full overflow-hidden rounded-full bg-muted',
indicator:
'h-full rounded-full transition-[width] duration-[--motion-duration-normal] ease-[--motion-ease-out]'
},
variants: {
segment: {
default: {},
segmented: {
track: 'flex gap-px'
}
}
},
defaultVariants: {
segment: 'default'
}
});
export type MeterSegment = VariantProps<typeof meterVariants>['segment'];
export type MeterProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
value: number;
min?: number;
max?: number;
low?: number;
high?: number;
optimum?: number;
segment?: MeterSegment;
/** Human-readable label for aria-valuetext. Defaults to "X%". */
valueText?: string;
};
/**
* Threshold-to-color rules (mirrors HTML <meter> spec semantics):
*
* If neither `low` nor `high` is set → bg-primary (always).
*
* When thresholds are set, the fill color reflects "how good is this value":
* - value < low → bg-destructive (sub-optimal / danger zone)
* - low <= value <= high → bg-warning (approaching limit)
* - value > high → bg-success (within safe range)
*
* `optimum` is a reference point that doesn't shift color; it represents
* the ideal value within the [min, max] range (matching the HTML spec).
*/
export function getMeterIndicatorColor(value: number, low?: number, high?: number): string {
if (low === undefined && high === undefined) return 'bg-primary';
if (low !== undefined && value < low) return 'bg-destructive';
if (high !== undefined && value > high) return 'bg-success';
return 'bg-warning';
}
</script>
<script lang="ts">
let {
ref = $bindable(null),
class: className,
value,
min = 0,
max = 100,
low,
high,
optimum,
segment = 'default',
valueText,
...restProps
}: MeterProps = $props();
const clampedValue = $derived(Math.min(Math.max(value, min), max));
const percentage = $derived(((clampedValue - min) / (max - min)) * 100);
const resolvedValueText = $derived(valueText ?? `${Math.round(percentage)}%`);
const indicatorColor = $derived(getMeterIndicatorColor(clampedValue, low, high));
const { root, track, indicator } = $derived(meterVariants({ segment }));
</script>
<div
bind:this={ref}
data-slot="meter"
role="meter"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={clampedValue}
aria-valuetext={resolvedValueText}
class={cn(root(), className)}
{...restProps}
>
<div data-slot="meter-track" class={track()}>
<div
data-slot="meter-indicator"
class={cn(indicator(), indicatorColor)}
style="width: {percentage}%"
></div>
</div>
</div>
Battery
Battery
18%Low battery — plug in to charge
meter-battery.svelte
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const meterVariants = tv({
slots: {
root: 'relative flex w-full flex-col gap-1',
track: 'relative h-2 w-full overflow-hidden rounded-full bg-muted',
indicator:
'h-full rounded-full transition-[width] duration-[--motion-duration-normal] ease-[--motion-ease-out]'
},
variants: {
segment: {
default: {},
segmented: {
track: 'flex gap-px'
}
}
},
defaultVariants: {
segment: 'default'
}
});
export type MeterSegment = VariantProps<typeof meterVariants>['segment'];
export type MeterProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
value: number;
min?: number;
max?: number;
low?: number;
high?: number;
optimum?: number;
segment?: MeterSegment;
/** Human-readable label for aria-valuetext. Defaults to "X%". */
valueText?: string;
};
/**
* Threshold-to-color rules (mirrors HTML <meter> spec semantics):
*
* If neither `low` nor `high` is set → bg-primary (always).
*
* When thresholds are set, the fill color reflects "how good is this value":
* - value < low → bg-destructive (sub-optimal / danger zone)
* - low <= value <= high → bg-warning (approaching limit)
* - value > high → bg-success (within safe range)
*
* `optimum` is a reference point that doesn't shift color; it represents
* the ideal value within the [min, max] range (matching the HTML spec).
*/
export function getMeterIndicatorColor(value: number, low?: number, high?: number): string {
if (low === undefined && high === undefined) return 'bg-primary';
if (low !== undefined && value < low) return 'bg-destructive';
if (high !== undefined && value > high) return 'bg-success';
return 'bg-warning';
}
</script>
<script lang="ts">
let {
ref = $bindable(null),
class: className,
value,
min = 0,
max = 100,
low,
high,
optimum,
segment = 'default',
valueText,
...restProps
}: MeterProps = $props();
const clampedValue = $derived(Math.min(Math.max(value, min), max));
const percentage = $derived(((clampedValue - min) / (max - min)) * 100);
const resolvedValueText = $derived(valueText ?? `${Math.round(percentage)}%`);
const indicatorColor = $derived(getMeterIndicatorColor(clampedValue, low, high));
const { root, track, indicator } = $derived(meterVariants({ segment }));
</script>
<div
bind:this={ref}
data-slot="meter"
role="meter"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={clampedValue}
aria-valuetext={resolvedValueText}
class={cn(root(), className)}
{...restProps}
>
<div data-slot="meter-track" class={track()}>
<div
data-slot="meter-indicator"
class={cn(indicator(), indicatorColor)}
style="width: {percentage}%"
></div>
</div>
</div>
Segmented
The segmented variant divides the track into discrete ticks,
useful when the data has natural quantization (e.g. signal bars, rating levels).
Signal strength 60%
Disk I/O 85%
meter-segmented.svelte
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const meterVariants = tv({
slots: {
root: 'relative flex w-full flex-col gap-1',
track: 'relative h-2 w-full overflow-hidden rounded-full bg-muted',
indicator:
'h-full rounded-full transition-[width] duration-[--motion-duration-normal] ease-[--motion-ease-out]'
},
variants: {
segment: {
default: {},
segmented: {
track: 'flex gap-px'
}
}
},
defaultVariants: {
segment: 'default'
}
});
export type MeterSegment = VariantProps<typeof meterVariants>['segment'];
export type MeterProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
value: number;
min?: number;
max?: number;
low?: number;
high?: number;
optimum?: number;
segment?: MeterSegment;
/** Human-readable label for aria-valuetext. Defaults to "X%". */
valueText?: string;
};
/**
* Threshold-to-color rules (mirrors HTML <meter> spec semantics):
*
* If neither `low` nor `high` is set → bg-primary (always).
*
* When thresholds are set, the fill color reflects "how good is this value":
* - value < low → bg-destructive (sub-optimal / danger zone)
* - low <= value <= high → bg-warning (approaching limit)
* - value > high → bg-success (within safe range)
*
* `optimum` is a reference point that doesn't shift color; it represents
* the ideal value within the [min, max] range (matching the HTML spec).
*/
export function getMeterIndicatorColor(value: number, low?: number, high?: number): string {
if (low === undefined && high === undefined) return 'bg-primary';
if (low !== undefined && value < low) return 'bg-destructive';
if (high !== undefined && value > high) return 'bg-success';
return 'bg-warning';
}
</script>
<script lang="ts">
let {
ref = $bindable(null),
class: className,
value,
min = 0,
max = 100,
low,
high,
optimum,
segment = 'default',
valueText,
...restProps
}: MeterProps = $props();
const clampedValue = $derived(Math.min(Math.max(value, min), max));
const percentage = $derived(((clampedValue - min) / (max - min)) * 100);
const resolvedValueText = $derived(valueText ?? `${Math.round(percentage)}%`);
const indicatorColor = $derived(getMeterIndicatorColor(clampedValue, low, high));
const { root, track, indicator } = $derived(meterVariants({ segment }));
</script>
<div
bind:this={ref}
data-slot="meter"
role="meter"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={clampedValue}
aria-valuetext={resolvedValueText}
class={cn(root(), className)}
{...restProps}
>
<div data-slot="meter-track" class={track()}>
<div
data-slot="meter-indicator"
class={cn(indicator(), indicatorColor)}
style="width: {percentage}%"
></div>
</div>
</div>
API reference
No children — the track and indicator are rendered internally. Use valueText to provide a human-readable label for assistive technology.
| Prop | Type | Default | Description |
|---|---|---|---|
number | — | Current measurement. Required. Clamped to [min, max] internally. | |
number | 0 | Lower bound of the range. | |
number | 100 | Upper bound of the range. | |
number | — | Threshold below which the value is considered sub-optimal (renders bg-destructive). | |
number | — | Threshold above which the value is considered optimal (renders bg-success). Values between low and high render bg-warning. | |
number | — | Ideal reference point within the range. Passed through to aria context but does not shift color (mirrors HTML <meter> spec). | |
'default' | 'segmented' | 'default' | Segmented shows discrete tick gaps along the track. | |
string | — | Human-readable aria-valuetext. Defaults to the percentage string, e.g. '50%'. | |
string | — | Merged onto the root wrapper via tailwind-merge. | |
HTMLDivElement | null | null | Two-way-bindable element reference. |