Gesso
Data display

Priority bars

Three rising bars whose fills are numbers, so any priority springs to any other.

Priority bars is a small mark for how urgent a task is: three rising bars that fill from the left as the priority goes up. Each bar's fill is a number on one spring, so any priority retargets any other, including mid-flight, and it is not five swapped icons or a CSS data-level snap. It is inked with system roles, and it names itself as an image, so it can sit in a row on its own or under a Chip trigger.

None

Click Advance, jump to a level, or hit Interrupt while a spring is in flight. Play cycle retargets every 220ms so the next priority lands mid-spring.

Usage

import { PriorityBars } from '@/components/ui/priority-bars';

<PriorityBars priority="high" />
import { PriorityBars } from '@/components/ui/priority-bars';

<button type="button" onClick={() => setPriority("urgent")}>
  <PriorityBars priority={priority} size="lg" />
</button>

The priorities are none, low, medium, high, and urgent. TASK_PRIORITY_LABELS holds the display text for each and PRIORITY_INK holds the color each is drawn in, as a CSS variable.

Five levels on three bars

Bars are filled from the left, and the unfilled part of each bar stays as a ghost track at 32% opacity (--alpha-32), so none still reads as three bars.

PriorityBars filledInk
none0--sys-fg-subtle
low1--sys-fg-muted
medium2--sys-cat-amber
high3--sys-cat-amber
urgent3, all at full height--destructive

Medium and high share the warning color, and bar count tells them apart. Urgent stands all three bars at full height instead of rising, so it differs from high by shape as well as ink.

None
Low
Medium
High
Urgent

The five levels. Mount jumps, so a list does not play in on first paint.

Three sizes

sm is 12px tall, md is 14px and the default, and lg is 18px. The bars get slightly wider with the size, from 2px at sm to 3px at lg.

sm
md
lg

Name it once

The mark is an image named Priority Low, Priority High, and so on, and it shows the same text as a native tooltip. That name is right when the mark is the only signal in the row. When the priority is also written next to it, a screen reader reads it twice, so pass label="" to hide the mark and let the text name the priority. Pass your own label for a translated name.

NoneLowMediumHighUrgent

Inside a chip the same rule applies, and the fix is the trigger's aria-label, as described on Chip trigger.

Motion

One story: priority to priority on a shared spring (stiffness 420 / damping 40 / mass 0.9), the same one Health face and Work mark use. It is faster than a --duration-base fade, with no bounce and no whole-mark fade.

RuleWhat happens
FillscaleY and opacity, with the origin at the bottom. The ghost track stays at 32%.
Level upChanging bars stagger left to right, 28ms apart (PRIORITY_BARS_STAGGER_MS).
Level downChanging bars stagger right to left. The first change is still immediate.
ColorMedium and high share --sys-cat-amber, and urgent is --destructive.
Mountinitial={false}, and the springs start on the current numbers.
Reduced motionjump to the end state, and the stagger drops to 0.
InterruptA new priority retargets the same springs.

See Motion for the overlay rules. Priority bars is a spring exception, and Tabs is the stretch-line exception.

Props

Priority bars takes only these props, and it forwards its ref to the root <span>.

PropTypeDefaultWhat it does
priority"none" | "low" | "medium" | "high" | "urgent"Required. Sets the bars, the ink, and the accessible name.
size"sm" | "md" | "lg""md"12, 14, or 18px tall.
labelstringPriority {level}The accessible name and tooltip. "" hides the mark from assistive tech.
classNamestringAdded to the root <span>.

Accessibility

  • The root is a <span role="img"> whose accessible name is built from the priority, like Priority High, and it carries the same text as a title, which browsers show as a tooltip on hover. The three bars inside are aria-hidden. label replaces the name, for a translated one.
  • When text beside the mark already says the priority, the image name repeats it, so pass label="", which makes the mark aria-hidden and drops the tooltip. A chip with Priority bars as its mark and no aria-label is read as "Priority High High", so give the trigger one that names the property and the value.
  • Bar count and shape carry the state, and color adds to it. Urgent's bars stand at full height where high's rise, so the pair holds up in grayscale and color blindness. In forced colors every ink becomes CanvasText, and the five levels are still told apart by bar count and shape.
  • The mark is not focusable and has no press behavior. Put it inside a button for that, and the button's name then includes the image name.
  • It is not an interactive target, so it has no 44px minimum. Size the control around it.
  • Under reduced motion the fills and the ink jump to the new priority with no transition and no stagger.

When to use something else

  • Badge for a priority written as a word, in a role color. It reads as text, so it needs no wrapper to avoid a double reading.
  • Category colors for a priority the user defines and colors themselves, like a select option named High, where the color stays the same in every look.
  • Health face for how a project is going and Work mark for where work stands, which are the other two marks drawn the same way.
  • Check mark for a plain completed check, not a graded level.
  • Avatar for a person, such as the assignee beside the priority.

Install

shadcn add @gesso/priority-bars

On this page