Skip to content
zabloo

ProgressBar

A track that fills to a fraction the game sets. It is the read-only half of the pair a Slider completes: the player watches this one rather than moving it.

primitivesince v1not focusable

A ProgressBar is a track that fills to a fraction between 0 and 1. The game sets that number and the player only reads it: health draining, a download filling, a reload timer running out. Picture a HUD whose value is a binding onto player.hp — every time the game calls SetData, the bar follows. It earns its place as a node type because nothing else in v1 can say a fraction of my parent: layout sizes are pixels, they are not bindable, and grow is neither.

progressbar-bars.viewIR v1
Three bars: a red Health bar filled to about two thirds and labelled 64 of 100, a gold Reload bar filled from the right, and a tall vertical purple Stamina bar filled from the bottom to about a third.
STATE
  • hover — off
  • pressed — off
  • focused — off
  • selected — off
  • disabled — off

Reported from the last painted frame

Not running — press Run to draw this on the GPU.

import { Column, ProgressBar, Row, Text } from "@zabloo/react";


const TRACK = {
  background: "{color.slot}",
  radius: "{radius.pill}",
} as const;

export default function ProgressBarBars() {
  return (
    <Column
      layout={{ grow: 1, justify: "center", align: "center", padding: "{space.6}" }}
      style={{ background: "{color.bg}" }}
    >
      <Row layout={{ gap: "{space.5}", align: "center" }}>
        <Column
          id="panel"
          layout={{ width: 320, padding: "{space.5}", gap: "{space.4}", align: "stretch" }}
          style={{
            background: "{color.surface}",
            radius: "{radius.lg}",
            borderWidth: "{border.hairline}",
            borderColor: "{color.line}",
          }}
        >
          <Column layout={{ gap: "{space.2}", align: "stretch" }}>
            <Row layout={{ justify: "space-between", align: "center" }}>
              <Text style={{ color: "{color.muted}", fontSize: "{text.xs}" }}>Health</Text>
              <Text
                bind="player.hpLabel"
                style={{ color: "{color.faint}", fontSize: "{text.xs}" }}
              />
            </Row>
            <ProgressBar
              id="hp"
              value={{ bind: "player.hp" }}
              size={8}
              transition={{ duration: "{motion.fast}" }}
              style={TRACK}
              fill={{ background: "{color.danger}", radius: "{radius.pill}" }}
            />
          </Column>

          <Column layout={{ gap: "{space.2}", align: "stretch" }}>
            <Text style={{ color: "{color.muted}", fontSize: "{text.xs}" }}>
              Reload · drains from the end
            </Text>
            {/* `justify: "end"` anchors the fill at the other side, which is
                what a bar that empties looks like. */}
            <ProgressBar
              id="reload"
              value={{ bind: "weapon.reload" }}
              size={8}
              layout={{ justify: "end" }}
              style={TRACK}
              fill={{ background: "{color.gold}", radius: "{radius.pill}" }}
            />
          </Column>
        </Column>

        {/* A column bar: the same node, main axis turned. */}
        <Column layout={{ gap: "{space.2}", align: "center" }}>
          <ProgressBar
            id="stamina"
            value={{ bind: "player.stamina" }}
            layout={{ direction: "column", justify: "end", height: 120 }}
            size={10}
            style={TRACK}
            fill={{ background: "{color.brand}", radius: "{radius.pill}" }}
          />
          <Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>Stamina</Text>
        </Column>
      </Row>
    </Column>
  );
}
Three bars, one node type. Look for what actually differs between them: the first grows from the left, the second is anchored at the far end and drains towards it, the third runs upward. That is a value, a direction and a justify — there is no prop here about progress.

The two tables below are further apart than their size suggests. size and fill are conveniences you write: the thickness becomes ordinary layout, and the fill’s style becomes the style of a real child node. Two props ship, and one of them is a slot.

Authoring props

<ProgressBar> emits this node with the fill already built — there is no raw export that would leave it to you, since the fill is children[0] by convention.

PropTypeDefaultDescription
valueBindable<number>0Progress in 0..1, usually a read binding.
fillStyleabsentStyle of the fill, merged over the default bar.
sizenumber8Bar thickness in px — height for a row bar, width for a column one.

On top of these, every component takes the node base props — id, visible, disabled, layout, style, states, transition, autofocus, clip — plus variant, which the theme resolves away at export time and which never appears in the IR. The component defaults to a horizontal bar (direction: "row") and to clip: true, so a square fill stays inside a rounded track.

IR props

What ships to the game, after the authoring layer is gone.

PropTypeDefaultDescription
valueBindable<number>0Progress in 0..1, clamped. Static or a read binding.
childrenZNode[][]children[0] = the fill. Further children are reserved.

Everything a bar looks like — the groove, the colour, the radius, the thickness — is the ordinary style of two ordinary nodes.

Geometry

The node is the track — its style paints the groove and its layout sizes it — and children[0] is the fill, an ordinary node whose own style paints the bar. A positional slot, like a Collapse’s header: paint stays implicit, so the fill is a composed child rather than a new draw command.

Along the main axis (layout.direction, which defaults to "column" like everywhere else in the format — the component sets "row" for you):

  • The SDK sizes the fill at contentMain × value, where contentMain is the track’s main axis minus its padding.
  • The fill stretches across the whole cross axis.
  • layout.justify anchors it: "start" (the default) grows from the left or top, "end" from the right or bottom — a bar that drains the other way — and "center" grows from the middle out.
  • The fill’s own width/height/grow on the main axis are ignored: the SDK owns that number.

children[1..] are reserved. v1 lays out nothing else inside the track — a label on top of the bar needs overlapping placement, which the format does not have.

A broken binding shows an empty bar

value is clamped to 0..1, and a non-finite one — missing data, a string, NaN — reads as 0. There is one answer to “what does a broken binding show”, and it is never a full bar and never a crash. (Reference implementation: clampProgress in @zabloo/format.)

Behavior

States

disabled only, inherited. Nothing else applies, because nothing else can happen to it.

Focusable

No, and there is nothing here for the player to do. A number the player sets by pointing is a Slider, and that is the whole difference between the two nodes.

Motion

A transition on this node tweens the value, not the computed rect: the SDK interpolates the fraction and then runs its normal layout pass with it, so there is still one pass per frame and both targets land on the same number. The fill’s own transition never sees the change, since its main size is not one of its declared inputs.

Actions

None — the traffic runs the other way. The game calls SetData on the bound path → the SDK re-reads the fraction → the fill re-sizes on the next frame. Nothing comes back.

Degradation

On an older SDK

On an older SDK the groove keeps its exact shape and the fill shrinks to nothing. The bar stops reporting; nothing around it moves.

As a Container: the track with an unsized fill inside it. The bar loses its fraction — never the layout around it, because the track's size was always its own. A HUD that degrades keeps its shape and stops reporting, which is the difference between a stale readout and a broken screen.

Composition

A bar with a label is a Column holding a Row and the bar — never a prop, because children[1..] is reserved and v1 has no overlapping placement.

  • A bound bar with a transition — the common case: the game owns the number, and the tween keeps a health drop from snapping.
  • A vertical bar — turn the main axis for a stamina gauge beside the screen edge.
  • justify: "end" — use it when the bar should drain towards its start, the way a reload timer reads.
  • A labelled bar — a Column with a Row above it, whenever the number needs saying in words as well.
// The common case: a read binding the game moves with SetData.
<ProgressBar value={{ bind: "player.hp" }} transition={{ duration: 200 }} />

// A vertical bar: the same node with its main axis turned.
<ProgressBar value={0.4} layout={{ direction: "column", height: 120 }} size={10} />

// Draining from the other side.
<ProgressBar value={{ bind: "weapon.reload" }} layout={{ justify: "end" }} />

// A labelled bar is composition, not a prop.
<Column layout={{ gap: 8, align: "stretch" }}>
<Row layout={{ justify: "space-between" }}>
  <Text>Health</Text>
  <Text bind="player.hpLabel" />
</Row>
<ProgressBar value={{ bind: "player.hp" }} fill={{ background: "{color.danger}" }} />
</Column>