AllocationBar
A pill split by share, with legend chips — selecting a segment changes what you are inspecting.
Usage
AllocationBar
Inventory allocation
Segmented bar, legend and detail panel, with the selection held by the host.
Best Practices
- Keep it to three to five segments and fold the tail into an "other"; the default ladder has four steps (accent plus three inks), so give the fifth segment an explicit
color. Anything under ~2% is neither clickable nor readable. - The default ladder already paints the headline share with the accent and leaves the rest in receding inks; pass
coloronly when the segments have real category colours — colouring everything erases the point of the bar. - Colour is never the only carrier: the legend always shows the code and the percentage, and the detail panel names the segment in full.
- Render the headline figure (
$51,785) yourself, reading it offsegmentsbymodelValue, so the card's height does not change with the selection.
API Reference
Props
| Name | Type | Default | Description |
|---|---|---|---|
segments | AllocationSegment[] | — | The segments, described below. |
modelValue | string | — | Selected segment key. Omit and the first one reads as active. |
legend | boolean | true | Renders the legend chips underneath. |
detail | boolean | false | Renders the detail panel (active segment's label and description). |
ariaLabel | string | 'Allocation segments' | Accessible name for the segment group. |
percentFormatter | (percent: number) => string | — | Overrides the default ${percent}%. |
AllocationSegment is { key, label, short?, percent, amount?, color?, description? }. short is the legend code (falling back to label), and percent runs 0–100.
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | (key: string) | A different segment was selected. |
change | (segment: AllocationSegment) | The same move, carrying the whole segment. |
Selecting Is Inspecting
The bar is not a read-only chart: clicking a segment — or its legend chip — changes which one you are inspecting, without the bar itself moving. That is what separates it from a progress bar: it says what the whole is made of, not how far along it is.
The selection is fully controlled. With no modelValue the first segment reads as active, but a click still only emits — the host either binds v-model or handles change itself.
amount travels with the data but the bar never paints it: it belongs to the card's headline figure, laid out by the host (see the example).
Overview
- The segments and the legend are two sets of controls over one value inside a single radio group: the wrapper is
role="radiogroup"with anaria-label, every segment and chip isrole="radio"reportingaria-checked, and only the selected item keepstabindex="0"; arrow keys move focus and the selection within the set that has focus (wrapping). The segments addaria-label="{label}: {percent}". - Clicking the active segment again emits nothing — there is no "deselect" here.
- A segment without
colorfalls back through an accent-then-receding-ink ladder by position:--tx-bui-accent→--tx-bui-ink→--tx-bui-ink-2→--tx-bui-ink-3, reusing the last step past four segments. That is how upstream expresses "colour the headline share, leave the rest grey"; the greys are the ink ramp rather than--tx-bui-line*because those sit a ΔRGB of 5–18 (≈1.03:1) from the track's--tx-bui-fieldand read as bare track in both themes. - Segment width stays exactly proportional to
percent: the track spends 2px of padding and a 2px gap, and each segment's slice of that gap budget is subtracted from its width up front (width: calc(percent% - its share of the gaps)), so the separators cost fixed pixels without redrawing the data; if the shares overrun 100% flex still shrinks them proportionally rather than clipping the last one. - The selected sheen is class-driven, not animation-driven: with motion reduced the transition is dropped and the sheen still appears immediately, with no blank gap.
- The easing is
cubic-bezier(0.16, 1, 0.3, 1)(upstream's--ease-link), heavier than this family's usual--tx-ease-out-strong. That difference is deliberate.
Technologies
- Component source:
packages/tuffex/packages/components/src/allocation-bar/src/TxAllocationBar.vue. - Types:
packages/tuffex/packages/components/src/allocation-bar/src/types.ts. - Tested coverage:
packages/tuffex/packages/components/src/allocation-bar/__tests__/allocation-bar.test.ts(17 cases) covers the exact rendered widths at the demo width once the gaps are carved out, accessible names and the radio-group semantics, arrow-key selection, the colour-ladder fallback and its contrast across both themes, the detail label's resolved colour, controlled selection, silence on a repeat click, legend/segment parity, the formatter, the detail panel switch, and the reduced-motion contract against compiled CSS. - Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT.
- Versus
TxProgressBar: a progress bar expresses one advancing quantity; this expresses composition. They look similar and mean different things, so they are not interchangeable. - The detail panel is additive: upstream puts that panel in the card rather than the bar. Here it is a
detailswitch, off by default, which keeps "bar plus legend" as the smallest reusable unit. - Reduced motion: the only element resting at
opacity: 0is the selection sheen, and it is lit by the.is-activeclass rather than an animation fill — a contract the tests pin separately. - Geometry: the gap budget is subtracted from each width in proportion to
percentup front rather than left to flex to shrink afterwards, so the declared share is the rendered share while the over-100% shrink fallback still holds. - Accessibility: the bar and the legend share one
radiogroup(the legend used to sit outside therole="group"), andaria-pressedbecamearia-checked— this expresses a single choice, not buttons that toggle independently.