Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

FeedbackReact

Meter Group

Shows several measurements that share one range on a single bar, such as storage used by apps, photos and the system.

Rendered by React 19.3.1

import * as React from 'react'
import { MeterGroup } from '@hamur/react/meter-group'

const kinds = ['Apps', 'Photos', 'System']

export default function MeterGroupExample() {
  const [value, setValue] = React.useState([20, 35, 12])
  const changePhotos = (by: number) =>
    setValue(([apps, photos, system]) => [
      apps,
      Math.max(0, Math.min(100 - apps - system, photos + by)),
      system,
    ])
  return (
    <div className="stack">
      <MeterGroup.Root className="range" value={value}>
        <div className="range-head">
          <MeterGroup.Label>Storage used</MeterGroup.Label>
          <MeterGroup.Value className="muted" />
        </div>
        <MeterGroup.Track className="range-track range-group-track">
          {kinds.map((kind, index) => (
            <MeterGroup.Indicator
              key={kind}
              className="range-group-fill"
              index={index}
            />
          ))}
        </MeterGroup.Track>
        <div className="range-group-legend">
          {kinds.map((kind, index) => (
            <MeterGroup.Item
              key={kind}
              className="range-group-item"
              index={index}
            >
              <MeterGroup.ItemLabel>{kind}</MeterGroup.ItemLabel>
              <MeterGroup.ItemValue className="muted" />
            </MeterGroup.Item>
          ))}
        </div>
      </MeterGroup.Root>
      <div className="row">
        <button type="button" className="button" onClick={() => changePhotos(-10)}>
          Delete photos
        </button>
        <button type="button" className="button" onClick={() => changePhotos(10)}>
          Import photos
        </button>
      </div>
    </div>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<MeterGroup.Root>
  <MeterGroup.Label />
  <MeterGroup.Value />
  <MeterGroup.Track>
    <MeterGroup.Indicator />
  </MeterGroup.Track>
  <MeterGroup.Item>
    <MeterGroup.ItemLabel />
    <MeterGroup.ItemValue />
  </MeterGroup.Item>
</MeterGroup.Root>

Examples

Overflow

A monthly budget in dollars that a trip pushes past its limit, with the total marked by [data-overflow].

MeterGroupOverflow.tsx
import * as React from 'react'
import { MeterGroup } from '@hamur/react/meter-group'

const categories = ['Rent', 'Food', 'Travel']
const format: Intl.NumberFormatOptions = {
  style: 'currency',
  currency: 'USD',
  maximumFractionDigits: 0,
}

export default function MeterGroupOverflowExample() {
  const [travel, setTravel] = React.useState(200)
  const value = [1200, 500, travel]
  return (
    <div className="stack">
      <MeterGroup.Root className="range" value={value} max={2000} format={format}>
        <div className="range-head">
          <MeterGroup.Label>Spent of $2,000</MeterGroup.Label>
          <MeterGroup.Value className="muted range-group-value" />
        </div>
        <MeterGroup.Track className="range-track range-group-track">
          {categories.map((category, index) => (
            <MeterGroup.Indicator key={category} className="range-group-fill" index={index} />
          ))}
        </MeterGroup.Track>
        <div className="range-group-legend">
          {categories.map((category, index) => (
            <MeterGroup.Item key={category} className="range-group-item" index={index}>
              <MeterGroup.ItemLabel>{category}</MeterGroup.ItemLabel>
              <MeterGroup.ItemValue className="muted" />
            </MeterGroup.Item>
          ))}
        </div>
      </MeterGroup.Root>
      <div className="row">
        <button type="button" className="button" onClick={() => setTravel(600)}>
          Book a trip
        </button>
        <button type="button" className="button" onClick={() => setTravel(200)}>
          Cancel the trip
        </button>
      </div>
    </div>
  )
}

API reference

10 props across 8 parts, in React syntax. className, style, render are listed once, under Shared props.

IndicatorMeterGroup.Indicator

Draws the segment of one value along the track. Renders a <div>.

Indicator props
PropTypeDefault
indexrequiredThe position in value of the amount this segment draws.number—

ItemMeterGroup.Item

The meter of one value, named by its item label: the group's legend entry. Renders a <div>.

Item props
PropTypeDefault
aria-valuetextA string value that provides a user-friendly name for aria-valuenow, the current value ofstring—
indexrequiredThe position in value of the amount this meter reads.number—

ItemLabelMeterGroup.ItemLabel

An accessible label for the meter. Renders a <span>.

ItemValueMeterGroup.ItemValue

A text element displaying the current value. Renders a <span>.

ItemValue props
PropTypeDefault
children((formattedValue: string, value: number) => React.ReactNode) | null—

LabelMeterGroup.Label

An accessible label for the group. Renders a <span>.

RootMeterGroup.Root

Groups the meters of several values that share one capacity, named by its label. Renders a <div>.

Root props
PropTypeDefault
formatOptions to format each value and the total.Intl.NumberFormatOptions—
getAriaValueTextA function that returns the aria-valuetext of each item, from its formatted value, its entryMeterGroupGetAriaValueText—
localeThe locale used by Intl.NumberFormat when formatting the values.Intl.LocalesArgument—
maxThe capacity every value is measured against.number100
valuerequiredThe amounts, in segment order.readonly number[]—

TrackMeterGroup.Track

Contains the indicators and represents the whole capacity. Renders a <div>.

ValueMeterGroup.Value

A text element displaying the total of the values. Renders a <span>.

Value props
PropTypeDefault
children((formattedValue: string, value: number) => React.ReactNode) | null—

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

MeterGroup takes no focus and has no keys of its own.

Accessibility

What the parts set
PartSetsSo that
Rootrole="group", aria-labelledby to Labelthe values are read as one named group
Itemrole="meter", aria-valuemin="0", aria-valuemax, aria-valuenow (clamped to max)a screen reader reads each value within its range
Itemaria-valuetext from format, getAriaValueText or aria-valuetext, aria-labelledby to ItemLabeleach value is read with its name and as written, such as "20%"
Valuethe formatted total as text, unclampedthe total is read in place, above the range of any one value
ItemValuearia-hidden="true"the visible number is not read twice

Focus order

  1. MeterGroup is not focusable; focus passes over it.