---
id: vanilla/units
title: Units
---

Units change **how a number renders**, never which string is selected — that is a context-param job. If a value would appear in a message branch (`{gender, select, …}`) it's a param; if it goes to a number formatter it's a unit. Declaring categories: [stift.config.ts](../configuration/config-file.md).

## Value contract: store in the base unit

| Category | Base |
|---|---|
| length | `meter` |
| mass | `kilogram` |
| area | `hectare` |
| temperature | `celsius` |
| digital | `byte` |

Data and messages carry base-unit numbers; the library converts base → active unit at render. Never branch messages on a unit system and never convert in the message.

## Reading and setting units (headless)

```ts
Stift.getUnits()                    // effective per-category record for the active locale
Stift.getUnitValues('length')       // allowed units — config supportedValues

Stift.setUnit('length', 'kilometer') // global override; validated against supportedValues
Stift.resetUnit('length')            // back to the per-locale default
```

`setUnit` validates — an invalid unit is rejected and reported via the runtime hooks. Only `setUnit` overrides are persisted (`unitsPersistence` in `createStift`); defaults come from config. Unconfigured categories are invisible: absent from `getUnits()`, `setUnit` rejected, empty `getUnitValues`. Per-locale defaults inherit down the dialect chain exactly like params.

## Formatting

`@secundus-studio/stift-format` does the rendering (state → render is deliberately split so the formatter is tree-shakable):

```ts
import { formatUnit, convertUnit } from '@secundus-studio/stift-format'

formatUnit(1000, { locale: 'en', unit: 'kilometer' })          // "1 km"
formatUnit(2.5, { locale: 'en', unit: 'pound', unitDisplay: 'long' }) // "5.5 pounds"
convertUnit(1, 'mile', 'meter')                                 // 1609.344
```

`unitDisplay?: 'long' | 'short' | 'narrow'`. Values are always in the category base unit — `formatUnit` receives meters regardless of the active unit. Wire the two layers together:

```ts
function formatDistance(meters: number) {
  return formatUnit(meters, { locale: Stift.getLocale(), unit: Stift.getUnits().length ?? 'meter' })
}
```

Subtree-level overrides (React's `useFormatter({ units })`) don't exist headlessly — just pass the `unit` per call.

## Common mistakes

- Passing a converted value to `formatUnit` (double conversion) — always pass base-unit values.
- Branching a message on a unit system (`{unitSystem, select, …}`) — units never select strings.
- Calling `setUnit` with a unit outside `supportedValues` — rejected.
- Reading `getUnits().length` for a category that isn't configured — absent by design.
- Forgetting `unitDisplay` for compact UI — long labels can overflow.

## Next

- The React hook surface (`useUnits`, pickers) → [Units](../react/units.md)
- Config declaration rules → [stift.config.ts](../configuration/config-file.md)
- Number/date/list formatting → [Translating & Formatting](./translating.md)
