Progress
A bar that fills up — a number, a percentage and nothing else.
src/components/rahti_ui/progress.rs
The bar
One prop, and the component draws the rest.
shadcn's own demo. A self-closing tag: the root renders its own track and indicator, so there is nothing to write for the bar itself. The `class` is the width — the base string says `w-full`, which is a starting point rather than an answer.
The fill is `width: 66%` in an inline style, not a transform. The registry still translates the indicator by `-(100 - value)%`; the current component sets a width, which is why the track needs `overflow-x-hidden` far less than it used to and why a value change animates from `transition-all` alone.
With a caption
A label on the left, a reading on the right, and the bar below both.
The children are the caption, and the track goes underneath them. That is `flex flex-wrap` doing the layout: the label and the value fit on one line, the `w-full` track cannot, so it wraps to the next. No grid and no rows.
`ml-auto` on the value is what pushes it to the far end, and `tabular-nums` is what stops the row twitching as the digits change — a `9` and a `1` are the same width in that font setting and different widths without it.
The value is passed twice, which shadcn does not have to do: its `<ProgressValue />` reads Base UI's context. There is no context here, so the number is repeated — the same divergence the Accordion's `name` and the Dropdown Menu's `menu` make.
Naming it
The one thing shadcn's markup leaves undone.
The root is the `progressbar` and the label beside it is `role="presentation"` — so the label alone does not name the bar. shadcn's own markup renders a label with an `id` and never points at it, which leaves a progressbar with no accessible name.
This port does not invent a prop for that. Give the label an `id` and pass `aria-labelledby` through the root's `attrs`, which is the one thing worth adding on top of shadcn's markup rather than reproducing as found.
Scales
Both ends, and a maximum that is not 100.
The last bar in the first group says `data-complete` where the others say `data-progressing`. Those are Base UI's names and they are documented rather than guessed — nothing in shadcn's class strings reads them, but a page can.
A `max` rescales everything: the bar, `aria-valuemax`, `aria-valuenow` and the caption. `value="1" max="4"` draws 25% and reads the same, because the width and the text are one number — what is drawn and what is announced cannot drift apart.
Controlled
A progress bar that can be controlled by a slider.
shadcn's controlled demo. Drag the slider and the bar follows — the label, the reading, the width, `aria-valuenow`, `aria-valuetext` and the state attribute all move together.
The script is the page's, not the component's, and that is the point worth taking from this example. React passes `value` down through props and both controls re-render; PulsePoint state does not cross an `html!` component boundary, so a page that wants two components to agree wires them in the DOM. Nothing here reaches inside a scope — it queries elements by `data-slot`, which is what those attributes are for.
It is also why the Slider being a native `<input type="range">` matters beyond that component's own page: there is a real element with a real `input` event, so this is twenty lines rather than a bridge between two JavaScript widgets.
Before the runtime mounts, the bar sits at the 50% the server drew and the slider still drags, still takes the keyboard and still posts its value — they simply stop agreeing. That is the whole cost of the missing script, and it is the only script on this page.
Indeterminate
No value, no claim — and the one thing this port leaves out.
Leave the value out and the bar makes no claim: `aria-valuenow` and `aria-valuetext` are gone entirely, which is how ARIA spells "unknown". Writing `aria-valuenow="0"` instead would say the bar is at the start, which is a different and wrong statement.
The root says `data-indeterminate` and the track draws nothing. Base UI animates a sliding fill here; that is not ported, because shadcn's class strings carry no keyframes for it and inventing some would be designing rather than porting. An empty trough is honest — it says as much as the component knows.
The port
Where these strings came from, and the nine places the registry disagrees.
Three components in a row where the published registry and the published page disagree. For the Alert and the Badge it was a stale set of class strings; here the whole component was rebuilt on Base UI, and a port made from the registry would be a different control with a different accessible tree — not merely one that looked dated.