Skip to content
Download ZIP

Blue good region palette · 6.9 KB ZIP File receipt View as Markdown View code

Preview

Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.

Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
cmp_bounded_meter_01 · version 1.0.0 · Blue good region palette
Using the PageSugar MCP server, fetch component cmp_bounded_meter_01 version 1.0.0 with variant "blue", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.

Not connected yet? Set up the MCP server

Code

Palette
Blue good region
Version
1.0.0
Digest
Full digest
sha256-a1dc18b0cc9f2355c0cc0e0eea528e89424a83b92e39a0ee6182d445dbbfd63e
Meter.svelte Svelte · 9.9 KB Raw
<script lang="ts" module>
	/** The three regions of the HTML meter algorithm; `critical` is the spec's "even less good". */
	export type MeterRegion = 'optimum' | 'suboptimal' | 'critical';
	export type MeterSize = 'thin' | 'regular';
	export type MeterPlacement = 'inline' | 'below';

	export interface MeterRange {
		value: number;
		min: number;
		max: number;
		low: number;
		high: number;
		optimum: number;
	}

	const finite = (n: number | undefined, fallback: number) =>
		typeof n === 'number' && Number.isFinite(n) ? n : fallback;
	const clamp = (n: number, lo: number, hi: number) => Math.min(Math.max(n, lo), hi);

	/**
	 * Normalise the attributes the way the HTML meter element does: max never below min, value
	 * clamped into the range, low within it, high between low and max, optimum within it and
	 * defaulting to the midpoint.
	 */
	export function resolveRange(input: {
		value: number;
		min?: number;
		max: number;
		low?: number;
		high?: number;
		optimum?: number;
	}): MeterRange {
		const min = finite(input.min, 0);
		const max = Math.max(min, finite(input.max, 1));
		const value = clamp(finite(input.value, 0), min, max);
		const low = clamp(finite(input.low, min), min, max);
		const high = clamp(finite(input.high, max), low, max);
		const optimum = clamp(finite(input.optimum, min / 2 + max / 2), min, max);
		return { value, min, max, low, high, optimum };
	}

	/** The gauge region of the HTML meter algorithm, with the boundaries browsers use. */
	export function meterRegion({ value, low, high, optimum }: MeterRange): MeterRegion {
		if (optimum < low) {
			if (value <= low) return 'optimum';
			return value <= high ? 'suboptimal' : 'critical';
		}
		if (optimum > high) {
			if (value >= high) return 'optimum';
			return value >= low ? 'suboptimal' : 'critical';
		}
		return value >= low && value <= high ? 'optimum' : 'suboptimal';
	}
</script>

<script lang="ts">
	interface Props {
		/** What is measured, shown above the bar and used as the meter's accessible name. */
		label: string;
		/** The current value. Values outside min and max are clamped for the bar and for aria. */
		value: number;
		/** Lower bound of the range. */
		min?: number;
		/** Upper bound of the range. */
		max: number;
		/** Upper edge of the low region. Defaults to min. */
		low?: number;
		/** Lower edge of the high region. Defaults to max. */
		high?: number;
		/** The preferred value: which region it falls in decides which region is good. */
		optimum?: number;
		/** Visible value with units, such as "7.2 GB of 10 GB"; also the aria-valuetext. */
		valueText?: string;
		/** A word for each region, such as "Nearly full", shown beside the value with a mark. */
		regionLabels?: Partial<Record<MeterRegion, string>>;
		/** One short line under the bar, such as when the quota resets. */
		description?: string;
		/** Cut the track at low and high so the regions show. */
		markers?: boolean;
		/** Bar height: thin is 4 px for dense lists, regular is 8 px. */
		size?: MeterSize;
		/** Value text on the label's line, or under the bar. */
		placement?: MeterPlacement;
		/** Extra classes on the root, for width or margins. */
		class?: string;
	}

	let {
		label,
		value,
		min = 0,
		max,
		low,
		high,
		optimum,
		valueText,
		regionLabels,
		description,
		markers = false,
		size = 'regular',
		placement = 'inline',
		class: className
	}: Props = $props();

	const uid = $props.id();

	const range = $derived(resolveRange({ value, min, max, low, high, optimum }));
	const region = $derived(meterRegion(range));
	const span = $derived(range.max - range.min);
	/* Halved before subtracting, so a range near the largest double cannot overflow to Infinity. */
	const percent = (n: number) =>
		span > 0 ? ((n / 2 - range.min / 2) / (range.max / 2 - range.min / 2)) * 100 : 0;
	const fill = $derived(percent(range.value));

	const shown = $derived(valueText ?? `${Math.round(fill)}%`);
	const regionText = $derived(regionLabels?.[region]);
	const ariaText = $derived(regionText ? `${shown}, ${regionText}` : shown);

	/* A threshold at either end of the range is not a boundary, so it is parked off the track. */
	const lowAt = $derived(range.low > range.min && range.low < range.max ? percent(range.low) : -10);
	const highAt = $derived(
		range.high > range.min && range.high < range.max ? percent(range.high) : 110
	);
	const marked = $derived(markers && (lowAt >= 0 || highAt <= 100));
	/*
	 * A cut the fill has not reached is a gap in a pale track, too faint to find, so a tick in the
	 * muted tone stands in it. Behind the fill the cut stays a clean gap.
	 */
	const ticks = $derived(
		marked ? [...new Set([lowAt, highAt])].filter((at) => at >= fill && at <= 100) : []
	);

	/* Each region reads one tone; the fill, the mark and the region word all follow it. */
	const TONES: Record<MeterRegion, string> = {
		optimum: '[--_tone:var(--_good)]',
		suboptimal: '[--_tone:var(--_warn)]',
		critical: '[--_tone:var(--_bad)]'
	};

	/* The fill never shrinks below a dot the height of the track, so a small value still shows. */
	const SIZES: Record<MeterSize, { track: string; fill: string; tick: string }> = {
		thin: { track: 'h-1', fill: 'min-w-1', tick: 'h-2' },
		regular: { track: 'h-2', fill: 'min-w-2', tick: 'h-3' }
	};

	const step = $derived(SIZES[size] ?? SIZES.regular);
</script>

{#snippet valueGroup()}
	<span class="flex min-w-0 flex-wrap items-center gap-x-2 text-sm leading-5 tabular-nums">
		<span class="text-(--_muted)">{shown}</span>
		{#if regionText || region !== 'optimum'}
			<!-- Outside the good region the mark shows even without a word, so tone is never colour alone. -->
			<span class="inline-flex items-center gap-1 font-semibold text-(--_tone)">
				<span class="flex shrink-0" aria-hidden="true">
					{@render mark()}
				</span>
				{regionText}
			</span>
		{/if}
	</span>
{/snippet}

{#snippet mark()}
	{#if region === 'optimum'}
		<svg
			class="size-3.5"
			viewBox="0 0 16 16"
			fill="none"
			stroke="currentColor"
			stroke-width="1.75"
			stroke-linecap="round"
			stroke-linejoin="round"
		>
			<path d="M3.5 8.25 6.5 11l6-6.5" />
		</svg>
	{:else if region === 'suboptimal'}
		<svg
			class="size-3.5"
			viewBox="0 0 16 16"
			fill="none"
			stroke="currentColor"
			stroke-width="1.75"
			stroke-linecap="round"
			stroke-linejoin="round"
		>
			<path d="M8 2.75 14 13H2L8 2.75Z" />
			<path d="M8 6.75v2.5M8 11.25v.01" />
		</svg>
	{:else}
		<svg class="size-3.5" viewBox="0 0 16 16" fill="currentColor">
			<path
				fill-rule="evenodd"
				d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM8.75 4.5v4.25h-1.5V4.5h1.5ZM8 11.75a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"
			/>
		</svg>
	{/if}
{/snippet}

<!-- A size container, so it takes its parent's full width rather than shrinking to content. -->
<div
	class={['bounded-meter @container w-full min-w-0', TONES[region], className]}
	data-region={region}
>
	<!-- In a narrow column the value always drops under the label, so a list of meters keeps one shape. -->
	<div
		class="flex flex-col gap-y-1 @xs:flex-row @xs:flex-wrap @xs:items-baseline @xs:justify-between @xs:gap-x-4"
	>
		<span
			id="{uid}-label"
			class="min-w-0 text-sm leading-5 font-semibold text-pretty break-words text-(--_ink)"
		>
			{label}
		</span>
		{#if placement === 'inline'}{@render valueGroup()}{/if}
	</div>

	<div class="relative mt-2 flex items-center">
		<div
			class={[
				'bounded-meter__track flex w-full overflow-hidden rounded-full bg-(--_track) rtl:[--_toward:to_left]',
				step.track,
				marked && 'bounded-meter__track--marked'
			]}
			style={marked ? `--_low: ${lowAt}%; --_high: ${highAt}%` : undefined}
			role="meter"
			aria-labelledby="{uid}-label"
			aria-describedby={description ? `${uid}-description` : undefined}
			aria-valuemin={range.min}
			aria-valuemax={range.max}
			aria-valuenow={range.value}
			aria-valuetext={ariaText}
		>
			{#if fill > 0}
				<div
					class={[
						'h-full rounded-full bg-(--_tone) transition-[width] duration-200 ease-(--_ease) motion-reduce:transition-none',
						step.fill
					]}
					style="width: {fill}%"
				></div>
			{/if}
		</div>
		{#each ticks as at (at)}
			<span
				class={['absolute w-0.5 rounded-full bg-(--_muted)', step.tick]}
				style="inset-inline-start: calc({at}% - 1px)"
				aria-hidden="true"
			></span>
		{/each}
	</div>

	{#if placement === 'below' || description}
		<div class="mt-2 flex flex-col gap-y-1">
			{#if placement === 'below'}{@render valueGroup()}{/if}
			{#if description}
				<p id="{uid}-description" class="min-w-0 text-xs leading-5 text-pretty text-(--_muted)">
					{description}
				</p>
			{/if}
		</div>
	{/if}
</div>

<style>
	/*
	 * Public tokens: set --bounded-meter-* on the meter or any ancestor to retone it. The good
	 * tone is near-black by default, so a quota that is fine reads as plain ink and only the
	 * warning and critical regions bring colour. Every tone clears 3:1 against the track and
	 * 4.5:1 as text on white.
	 */
	.bounded-meter {
		--_good: var(--bounded-meter-good, #1d4ed8);
		--_warn: var(--bounded-meter-warn, #b45309);
		--_bad: var(--bounded-meter-bad, #b91c1c);
		--_track: var(--bounded-meter-track, rgb(0 0 0 / 0.08));
		--_ink: var(--bounded-meter-ink, #18181b);
		--_muted: var(--bounded-meter-muted, #52525b);
		--_ease: cubic-bezier(0.2, 0, 0, 1);
	}

	/*
	 * Formula: the thresholds are cut out of the track with a mask, a 2 px gap at low and high,
	 * so the regions read on any background without a surface token. The gradient runs with the
	 * inline direction: the rtl: utility on the track flips --_toward, so in right-to-left text
	 * the cuts sit where the fill reaches them. (A :dir() rule here gets rewritten to a :lang()
	 * list by some CSS builds, which misses a dir="rtl" page in another language.)
	 */
	.bounded-meter__track--marked {
		--_cut: 1px;
		--_cuts:
			#000 calc(var(--_low) - var(--_cut)), transparent 0 calc(var(--_low) + var(--_cut)),
			#000 0 calc(var(--_high) - var(--_cut)), transparent 0 calc(var(--_high) + var(--_cut)),
			#000 0;
		mask-image: linear-gradient(var(--_toward, to right), var(--_cuts));
	}
</style>

Pass a label, value and max, plus low, high and optimum to say which region is good; give valueText with units and regionLabels for the words. It renders one static measurement. It is not a progress bar for loading or uploads, does not compute or fetch usage, and does not animate on load.

Suggested location
src/lib/components/bounded-meter-01
Required props
labelvaluemax

Limitations

  • Without regionLabels, a warning or critical region shows its colour, a shape mark and the value text, but no word, and screen readers hear only the value. Give a word for every region your thresholds can reach.
  • Without valueText, the visible text and aria-valuetext are a rounded percentage. Pass valueText with units in the reader's language; the component does not format numbers.
  • The value clamps to min and max for the bar and aria-valuenow, as the HTML meter does. Say an over-limit figure in valueText; the bar cannot show it.
  • Threshold cuts are a 2 px CSS mask, so they show whatever is behind the meter. Direction comes from Tailwind's rtl: variant (a dir="rtl" ancestor).
  • Light pages by default. On a dark page set --bounded-meter-good, -ink, -muted and -track to light values.

Example

Svelte
<script lang="ts">
	import Meter from '$lib/components/bounded-meter-01/Meter.svelte';
</script>

<Meter
	label="File storage"
	value={7.2}
	max={10}
	low={8}
	high={9.5}
	optimum={0}
	valueText="7.2 GB of 10 GB"
	regionLabels={{ suboptimal: 'Nearly full', critical: 'Full' }}
	description="Attachments and exports across every board."
	markers
/>

The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.

Bounded meter#

A meter shows where one measurement sits inside a known range: storage against a plan limit, a password score, a pool temperature. It is not a progress bar. Nothing is being completed, so use a progress component for uploads and loading.

Choosing low, high and optimum#

The three numbers follow the HTML <meter> element, and the tone follows its algorithm:

What you are measuring Set Regions
A quota, where less is better optimum below low good up to low, warning to high, critical above
A score, where more is better optimum above high good from high, warning from low, critical below
A reading that should stay inside optimum between them good between low and high, warning at both ends

Values on a boundary count as the better region, as browsers do. With no low or high, the whole range is one region and the meter stays in the good tone.

Say it in words#

Colour alone is not enough. Pass valueText with units in the reader's language, and a regionLabels word for every region your thresholds can reach:

Svelte
<Meter
	label="Automation runs"
	value={4410}
	max={5000}
	low={4000}
	high={4750}
	optimum={0}
	valueText="4,410 of 5,000"
	regionLabels={{ suboptimal: 'Near limit', critical: 'Almost out' }}
	description="Resets on 1 November."
	markers
/>

If the value can exceed max, keep the true figure in valueText ("11.4 GB of 10 GB"); the bar and aria-valuenow stop at max.

Lists of meters#

On a usage page, put each meter in a row of one bordered panel with hairline dividers between rows. Use size="thin" and leave markers off when there are more than four or five.

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
labelstringYesNoneWhat is measured. Shown above the bar and used as the meter's accessible name.
valuenumberYesNoneThe current value. Clamped to min and max for the bar and aria-valuenow.
minnumberNo0Lower bound of the range.
maxnumberYesNoneUpper bound of the range. A max below min is raised to min.
lownumberNominUpper edge of the low region. Defaults to min; clamped into the range.
highnumberNomaxLower edge of the high region. Defaults to max; clamped between low and max.
optimumnumberNomidpoint of min and maxThe preferred value. Below low means low is good (a quota), above high means high is good (a strength score), between them means both ends warn.
valueTextstringNoNoneVisible value with units, such as "7.2 GB of 10 GB". Also the aria-valuetext. Omitted, a rounded percentage is shown.
regionLabelsPartial<Record<'optimum' | 'suboptimal' | 'critical', string>>NoNoneA word for each region, shown after the value with a shape mark and appended to aria-valuetext. Regions without a word show none.
descriptionstringNoNoneOne short line under the bar, such as when the quota resets. Linked with aria-describedby.
markersbooleanNofalseCut the track at low and high so the regions show; a threshold ahead of the fill also gets a tick in the muted tone. Thresholds at min or max are not cut.
size'thin' | 'regular'No'regular'Bar height: thin is 4 px for dense lists, regular is 8 px.
placement'inline' | 'below'No'inline'Value text at the end of the label's line, or under the bar.
classstringNoNoneExtra classes on the root, for width or margins.

Customization#

On this page

Retone through six --bounded-meter-* CSS variables: one per region tone, the track, and two text tones. Thresholds, size and placement are props.

  • Brand colour for the good region: set --bounded-meter-good (near-black by default, so a healthy quota reads as plain ink). A green suits scores where high is good; keep it at 3:1 against the track and 4.5:1 as text.
  • Warning and critical: --bounded-meter-warn (amber-700) and --bounded-meter-bad (red-700) colour the fill, the mark and the region word.
  • Track: --bounded-meter-track is an 8% black tint, so it works on white, zinc-50 or a cream page.
  • Text: --bounded-meter-ink is the label, --bounded-meter-muted the value text and description.
  • Dark or tinted page: set --bounded-meter-good to #fafafa, --bounded-meter-warn to #fbbf24, --bounded-meter-bad to #f87171, --bounded-meter-track to rgb(255 255 255 / 0.12), --bounded-meter-ink to #fafafa and --bounded-meter-muted to #a1a1aa.
  • Thresholds: a quota sets optimum below low (optimum={0}); a strength score sets it above high; a reading that should stay in a band puts it between low and high.
  • Dense lists: size="thin" and no markers; one meter on its own: regular with markers and a description.
  • Width: the meter fills its parent. Pass class="max-w-sm" or put it in a grid cell to bound it.
  • Threshold ticks use --bounded-meter-muted, so they follow the value text's tone.

Public CSS variables

VariableToken
--bounded-meter-goodgood
--bounded-meter-warnwarn
--bounded-meter-badbad
--bounded-meter-tracktrack
--bounded-meter-inkink
--bounded-meter-mutedmuted

Accessibility#

On this page
  • The bar is a div with role="meter" (WAI-ARIA 1.2), named by the visible label through aria-labelledby, with aria-valuemin, aria-valuemax, aria-valuenow and aria-valuetext. It is not a progressbar and is not focusable.
  • aria-valuetext is valueText plus the current region word, so units and state are announced. Write valueText in words a screen reader reads well ("GB" is fine; avoid symbols only).
  • The region is shown by colour and a shape mark (a triangle for warning, a filled circle for critical, a check for good when it has a word), plus a word from regionLabels. Give words for every reachable region; the marks are hidden from assistive technology.
  • The description line is linked with aria-describedby.
  • Default tones clear 3:1 against the track and 4.5:1 as text on white. Re-check any colour you set.
  • The fill's width transition is turned off under prefers-reduced-motion, and nothing animates on first render.

Known limitations

  • Support for role="meter" varies by screen reader; older ones announce it as a generic element or a progress bar. The visible value text keeps the meaning available either way.

Release details#

On this page
Integration
  • Presentational
  • Works without client-side JavaScript
  • Server-side rendering supported
Dependencies
No additional runtime packages beyond Svelte and Tailwind CSS
License

MIT. Default license approval is pending; see the license status before adopting the source.

Version history
  • 1.0.0 (Published) Current release · 21 October 2026

Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.