Components/AllocationBar

AllocationBar

A pill split by share, with legend chips — selecting a segment changes what you are inspecting.

VerifiedSince 0.3.9

Usage

AllocationBar

Inventory allocation

Segmented bar, legend and detail panel, with the selection held by the host.

Loading demo...

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 color only 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 off segments by modelValue, so the card's height does not change with the selection.

API Reference

Props

NameTypeDefaultDescription
segmentsAllocationSegment[]—The segments, described below.
modelValuestring—Selected segment key. Omit and the first one reads as active.
legendbooleantrueRenders the legend chips underneath.
detailbooleanfalseRenders the detail panel (active segment's label and description).
ariaLabelstring'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

EventPayloadDescription
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 an aria-label, every segment and chip is role="radio" reporting aria-checked, and only the selected item keeps tabindex="0"; arrow keys move focus and the selection within the set that has focus (wrapping). The segments add aria-label="{label}: {percent}".
  • Clicking the active segment again emits nothing — there is no "deselect" here.
  • A segment without color falls 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-field and 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.
查看源码
packages/tuffex/packages/components/src/allocation-bar/index.ts
  • 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 detail switch, off by default, which keeps "bar plus legend" as the smallest reusable unit.
  • Reduced motion: the only element resting at opacity: 0 is the selection sheen, and it is lit by the .is-active class rather than an animation fill — a contract the tests pin separately.
  • Geometry: the gap budget is subtracted from each width in proportion to percent up 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 the role="group"), and aria-pressed became aria-checked — this expresses a single choice, not buttons that toggle independently.