ProgressBar
Un track que se llena hasta la fracción que fija el juego. Es la mitad de solo lectura del par que completa un Slider: esta el jugador la mira en vez de moverla.
primitivedesde v1sin focus
Una ProgressBar es un track que se llena hasta una fracción entre 0 y 1. El
juego fija ese número y el jugador solo lo lee: la vida que baja, una descarga
que avanza, un temporizador de recarga que se agota. Piensa en un HUD cuyo
value es un binding a player.hp: cada vez que el
juego llama a SetData, la barra le sigue. Se gana su sitio como tipo de nodo
porque en la v1 no hay nada más que sepa decir una fracción de mi padre: los
tamaños del layout son píxeles, no son enlazables, y grow tampoco.

- hover — inactivo
- pressed — inactivo
- focused — inactivo
- selected — inactivo
- disabled — inactivo
Se lee del último frame dibujado
Parado: pulsa Ejecutar para dibujarlo en la 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>
);
}{
"v": 1,
"tokens": {
"color.bg": "#0b0d13",
"color.surface": "#0e1016",
"color.raised": "#14141a",
"color.line": "#ffffff1f",
"color.line-strong": "#ffffff24",
"color.text": "#ffffff",
"color.muted": "#a1a1aa",
"color.faint": "#8a8a93",
"color.on-brand": "#ffffff",
"color.brand": "#8b5cf6",
"color.brand-strong": "#7c3aed",
"color.brand-hover": "#8b5cf6",
"color.brand-pressed": "#6d28d9",
"color.brand-soft": "#8b5cf61f",
"color.gold": "#fcd34d",
"color.slot": "#ffffff08",
"color.danger": "#f87171",
"radius.sm": 6,
"radius.md": 10,
"radius.lg": 14,
"radius.pill": 999,
"border.hairline": 1,
"border.focus": 2,
"space.1": 4,
"space.2": 8,
"space.3": 12,
"space.4": 16,
"space.5": 20,
"space.6": 24,
"text.xs": 11,
"text.sm": 13,
"text.md": 15,
"text.lg": 17,
"motion.fast": 120,
"motion.loop": 900
},
"views": {
"progressbar-bars": {
"type": "Container",
"layout": {
"direction": "column",
"grow": 1,
"justify": "center",
"align": "center",
"padding": "{space.6}"
},
"style": {
"background": "{color.bg}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"gap": "{space.5}",
"align": "center"
},
"children": [
{
"type": "Container",
"id": "panel",
"layout": {
"direction": "column",
"width": 320,
"padding": "{space.5}",
"gap": "{space.4}",
"align": "stretch"
},
"style": {
"background": "{color.surface}",
"radius": "{radius.lg}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "stretch"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.xs}"
},
"text": "Health"
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": {
"bind": "player.hpLabel"
}
}
]
},
{
"type": "ProgressBar",
"id": "hp",
"layout": {
"direction": "row",
"height": 8
},
"style": {
"background": "{color.slot}",
"radius": "{radius.pill}"
},
"transition": {
"duration": "{motion.fast}"
},
"clip": true,
"value": {
"bind": "player.hp"
},
"children": [
{
"type": "Container",
"style": {
"background": "{color.danger}",
"radius": "{radius.pill}"
}
}
]
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "stretch"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.xs}"
},
"text": "Reload · drains from the end"
},
{
"type": "ProgressBar",
"id": "reload",
"layout": {
"direction": "row",
"height": 8,
"justify": "end"
},
"style": {
"background": "{color.slot}",
"radius": "{radius.pill}"
},
"clip": true,
"value": {
"bind": "weapon.reload"
},
"children": [
{
"type": "Container",
"style": {
"background": "{color.gold}",
"radius": "{radius.pill}"
}
}
]
}
]
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "center"
},
"children": [
{
"type": "ProgressBar",
"id": "stamina",
"layout": {
"direction": "column",
"width": 10,
"justify": "end",
"height": 120
},
"style": {
"background": "{color.slot}",
"radius": "{radius.pill}"
},
"clip": true,
"value": {
"bind": "player.stamina"
},
"children": [
{
"type": "Container",
"style": {
"background": "{color.brand}",
"radius": "{radius.pill}"
}
}
]
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": "Stamina"
}
]
}
]
}
]
}
}
}Las dos tablas de abajo están más lejos entre sí de lo que su tamaño sugiere.
size y fill son comodidades que escribes tú: el grosor se convierte en
layout normal, y el estilo del relleno se convierte en el estilo de un nodo
hijo de verdad. Se publican dos props, y una de ellas es un slot.
Props de autoría
<ProgressBar> emite este nodo con el fill ya construido — no hay una
exportación en crudo que te lo deje a ti, porque el fill es children[0] por
convención.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| value | Bindable<number> | 0 | Progreso entre 0 y 1, normalmente un binding de lectura. |
| fill | Style | absent | Estilo del fill, mezclado sobre la barra por defecto. |
| size | number | 8 | Grosor de la barra en px — alto si va en fila, ancho si va en columna. |
Además de estas, todo componente acepta las props base del nodo —id,
visible, disabled, layout, style, states, transition, autofocus,
clip— más variant, que el theme resuelve al exportar y que nunca aparece en
la IR. El componente viene por defecto como barra horizontal
(direction: "row") y con clip: true, para que un fill cuadrado se quede
dentro de un track redondeado.
Props de IR
Lo que llega al juego, cuando ya no queda capa de autoría.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| value | Bindable<number> | 0 | Progreso entre 0 y 1, acotado. Estático o un binding de lectura. |
| children | ZNode[] | [] | children[0] = el fill. Los demás hijos están reservados. |
Todo el aspecto de una barra —la ranura, el color, el radio, el grosor— es el estilo normal de dos nodos normales.
La geometría
El nodo es el track —su style pinta la ranura y su layout lo
dimensiona— y children[0] es el fill, un nodo normal cuyo propio estilo
pinta la barra. Un slot posicional, como la cabecera de un Collapse: el pintado
sigue siendo implícito, así que el fill es un hijo compuesto y no un comando de
dibujo nuevo.
En el eje principal (layout.direction, que por defecto es "column" como en
todo el resto del formato — el componente te pone "row"):
- El SDK dimensiona el fill a
contentMain × value, dondecontentMaines el eje principal del track menos supadding. - El fill se estira a lo largo de todo el eje transversal.
layout.justifylo ancla:"start"(el valor por defecto) crece desde la izquierda o desde arriba,"end"desde la derecha o desde abajo —una barra que se vacía al revés— y"center"crece desde el medio hacia fuera.- El
width/height/growpropios del fill en el eje principal se ignoran: ese número es del SDK.
children[1..] están reservados. La v1 no coloca nada más dentro del track — una
etiqueta encima de la barra necesitaría colocación superpuesta, que el formato no
tiene.
value se acota a 0..1, y uno no finito —datos que faltan, una cadena, NaN— se lee como
0. Hay una sola respuesta a “qué enseña un binding roto”, y no es nunca una barra llena ni
un crash. (Implementación de referencia: clampProgress, en @zabloo/format.)
Comportamiento
- Estados
Solo
disabled, heredado. No se aplica nada más, porque no le puede pasar nada más.- Focusable
No, y aquí no hay nada que el jugador pueda hacer. Un número que fija el jugador señalando es un
Slider, y esa es la diferencia entera entre los dos nodos.- Movimiento
Una
transitionen este nodo interpola el valor, no el rect calculado: el SDK interpola la fracción y luego corre su paso de layout normal con ella, así que sigue habiendo un solo paso por frame y los dos targets aterrizan en el mismo número. Latransitionpropia del fill no llega a ver el cambio, porque su tamaño principal no es una de sus entradas declaradas.- Actions
Ninguna — aquí el tráfico va en el otro sentido. El juego llama a
SetDatasobre el path enlazado → el SDK vuelve a leer la fracción → el fill se redimensiona en el siguiente frame. No vuelve nada.
Degradación
En un SDK más antiguo la ranura conserva su forma exacta y el fill se queda en nada. La barra deja de informar; a su alrededor no se mueve nada.
Como Container: el track con un fill sin tamaño dentro. La barra pierde su fracción — nunca el layout de alrededor, porque el tamaño del track siempre fue suyo. Un HUD que degrada conserva su forma y deja de informar, que es la diferencia entre una lectura vieja y una pantalla rota.
Composición
Una barra con etiqueta es un Column que contiene un Row y la barra, nunca una
prop, porque children[1..] está reservado y la v1 no tiene colocación
superpuesta.
- Una barra enlazada con
transition— el caso común: el juego es dueño del número, y la interpolación evita que un golpe de vida dé un salto seco. - Una barra vertical — gira el eje principal para un medidor de aguante pegado al borde de la pantalla.
justify: "end"— cuando la barra deba vaciarse hacia su principio, que es como se lee un temporizador de recarga.- Una barra con etiqueta — un
Columncon unRowencima, siempre que el número haya que decirlo también con palabras.
// 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>