Progress
Displays a determinate progress indicator with an optional threshold tone and a striped or shimmer animated fill.
Preview
Uploading… (60%)
Working… (indeterminate)
<div class="w-full max-w-sm space-y-6">
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Uploading…') }} (60%)</p>
<x-ui.progress value="60" />
</div>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Working…') }} ({{ __('indeterminate') }})</p>
<x-ui.progress indeterminate />
</div>
</div>
Tone options
Size options
Installation
php artisan ui:add progress
Registry contract
php artisan ui:add progress
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/progress.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Progress (`progress`)
Displays a determinate progress indicator with an optional threshold tone and a striped or shimmer animated fill.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add progress
```
## Usage
```blade
<div class="w-full max-w-sm space-y-6">
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Uploading…') }} (60%)</p>
<x-ui.progress value="60" />
</div>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Working…') }} ({{ __('indeterminate') }})</p>
<x-ui.progress indeterminate />
</div>
</div>
```
## Props
- `value` (int, default `0`) — Current value, clamped between 0 and max.
- `max` (int, default `100`) — Maximum value the indicator represents.
- `indeterminate` (bool, default `false`) — Renders an animated marquee fill instead of a determinate width.
- `tone` (neutral|primary|success|warning|destructive|default, default `neutral`) — Fill colour: neutral (the foreground colour, so primary stays reserved for the main action and focus), primary (the brand primary), or the threshold tones success, warning and destructive. default is kept as an alias of neutral.
- `animation` (string, default `none`) — Decorative overlay on the determinate fill: none, striped, or shimmer; ignored while indeterminate.
- `label` (mixed|null, default `null`) — Accessible name for the progressbar; falls back to a generic "Progress" label when no aria-label/aria-labelledby is supplied.
- `bind` (mixed|null, default `null`) — An Alpine expression for a live value; the indicator width and aria-valuenow follow it instead of the static value prop.
- `size` (default|edge, default `default`) — Track shape: default is the 8px rounded bar; edge is a 2px square-ended strip (data-size="edge") that sits on the top edge of a panel or row while background work runs. Position it with the consumer's class (for example absolute inset-x-0 top-0 inside a relative parent); the strip drops its own relative position when that class sets absolute, fixed or sticky. Works with indeterminate, tone and bind.
## Use when
- Use to show real progress, ordered stages, or process status when visibility helps completion.
- Showing a determinate percentage-complete value for uploads, wizards, or long-running tasks.
- Needing an indeterminate marquee state before a real percentage is known.
## Avoid when
- Do not add progress UI to tiny tasks or to flows whose steps are not meaningfully sequential.
- Showing a looping, self-contained onboarding checklist; use onboard-card instead.
- Indicating a generic busy state with no measurable percentage; use spinner or a loader component instead.
## Anti-patterns
- Decorative or inaccurate progress
- Steppers for nonsequential exploration
## Rules
- Use the `<brok:progress>` tag (or `<x-ui.progress>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/progress
- Registry JSON (files, props, contract): https://brokui.dev/r/open/progress.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- size="edge": a 2px strip on the top edge of a panel or a row while background work runs. The row keeps a visible text status, so the strip is never the only signal. --}}
<div class="flex w-full max-w-md flex-col gap-4">
<div class="relative overflow-hidden rounded-lg border border-border bg-card p-4">
<x-ui.progress size="edge" indeterminate :label="__('Extracting text')" class="absolute inset-x-0 top-0" />
<p class="text-sm font-medium text-foreground">{{ __('Quarterly report.pdf') }}</p>
<p class="mt-1 text-sm text-muted-foreground">{{ __('Extracting text…') }}</p>
</div>
<div class="relative overflow-hidden rounded-lg border border-border bg-card p-4">
<x-ui.progress size="edge" value="45" tone="primary" :label="__('Upload progress')" class="absolute inset-x-0 top-0" />
<p class="text-sm font-medium text-foreground">{{ __('Product demo.mp4') }}</p>
<p class="mt-1 text-sm text-muted-foreground">{{ __('Uploading, 45%') }}</p>
</div>
</div>
{{-- bind: the fill follows an Alpine value, so it glides to each new step and aria-valuenow follows it. --}}
<div x-data="{ step: 1 }" class="flex w-64 flex-col gap-4">
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Workspace setup, step') }} <span x-text="step">1</span> {{ __('of 4') }}</p>
<x-ui.progress value="25" bind="step * 25" :label="__('Workspace setup')" />
</div>
<div class="flex gap-2">
<x-ui.button variant="outline" size="sm" data-testid="progress-back" x-bind:disabled="step === 1" x-on:click="step = Math.max(1, step - 1)">{{ __('Back') }}</x-ui.button>
<x-ui.button size="sm" data-testid="progress-next" x-bind:disabled="step === 4" x-on:click="step = Math.min(4, step + 1)">{{ __('Next step') }}</x-ui.button>
</div>
</div>
<div class="w-full max-w-sm space-y-6">
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Import: 1,240 of 2,000 rows (neutral, the default)') }}</p>
<x-ui.progress value="62" :label="__('Import progress')" />
</div>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Import: 1,240 of 2,000 rows (primary)') }}</p>
<x-ui.progress value="62" tone="primary" :label="__('Import progress, primary tone')" />
</div>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Storage: 91% used (destructive)') }}</p>
<x-ui.progress value="91" tone="destructive" :label="__('Storage used')" />
</div>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | int | 0 | Current value, clamped between 0 and max. |
| max | int | 100 | Maximum value the indicator represents. |
| indeterminate | bool | false | Renders an animated marquee fill instead of a determinate width. |
| tone | neutral | primary | success | warning | destructive | default | neutral | Fill colour: neutral (the foreground colour, so primary stays reserved for the main action and focus), primary (the brand primary), or the threshold tones success, warning and destructive. default is kept as an alias of neutral. |
| animation | string | none | Decorative overlay on the determinate fill: none, striped, or shimmer; ignored while indeterminate. |
| label | mixed | null | null | Accessible name for the progressbar; falls back to a generic "Progress" label when no aria-label/aria-labelledby is supplied. |
| bind | mixed | null | null | An Alpine expression for a live value; the indicator width and aria-valuenow follow it instead of the static value prop. |
| size | default | edge | default | Track shape: default is the 8px rounded bar; edge is a 2px square-ended strip (data-size="edge") that sits on the top edge of a panel or row while background work runs. Position it with the consumer's class (for example absolute inset-x-0 top-0 inside a relative parent); the strip drops its own relative position when that class sets absolute, fixed or sticky. Works with indeterminate, tone and bind. |
Slots
default— Unused; the component has no visible default slot content.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- The value is clamped between 0 and max and converted to a percentage width, transitioning over 300ms ease-out.
- When bind is set, the fill width and aria-valuenow track the Alpine expression live instead of the static value prop.
- size="edge" keeps the same progressbar semantics, value and indeterminate marquee on a 2px strip; under reduced motion the indeterminate strip is a still, full-width bar.
- The fill defaults to the neutral foreground colour; tone="primary" restores the brand fill. The default palette looks the same either way, because its primary is the text colour.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Communicate genuine progress or ordered stages.
Use when
- Use to show real progress, ordered stages, or process status when visibility helps completion.
- Showing a determinate percentage-complete value for uploads, wizards, or long-running tasks.
- Needing an indeterminate marquee state before a real percentage is known.
Avoid when
- Do not add progress UI to tiny tasks or to flows whose steps are not meaningfully sequential.
- Showing a looping, self-contained onboarding checklist; use onboard-card instead.
- Indicating a generic busy state with no measurable percentage; use spinner or a loader component instead.
Use instead
- Simple completion text for tiny tasks
Anti-patterns
- Decorative or inaccurate progress
- Steppers for nonsequential exploration
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- managed
- Focus
none
- The root carries role="progressbar" with aria-valuemin/aria-valuemax and, unless indeterminate, aria-valuenow; supply label, aria-label, or aria-labelledby so every instance has an accessible name.
- Reduced motion removes the fill's width transition and the striped/shimmer overlay animation.
- An edge strip is 2px tall, so it must not be the only status signal: keep a label on the progressbar and a visible text status nearby when the work matters to the task.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="progress-{{ $record->id }}">
<div class="w-full max-w-sm space-y-6">
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Uploading…') }} (60%)</p>
<x-ui.progress value="60" />
</div>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ __('Working…') }} ({{ __('indeterminate') }})</p>
<x-ui.progress indeterminate />
</div>
</div>
</div>
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'value' => 0,
'max' => 100,
'indeterminate' => false,
// neutral (default) | primary | success | warning | destructive. `default`
// is kept as an alias of neutral for 1.x callers.
'tone' => 'neutral',
// 'none' (default) | 'striped' | 'shimmer' — decorative motion over the
// determinate fill. Ignored while indeterminate (which has its own marquee).
'animation' => 'none',
// Accessible name — role="progressbar" requires one (WCAG). Pass a
// meaningful label in context; falls back to a generic name. Skipped when
// the consumer supplies aria-label/aria-labelledby themselves.
'label' => null,
// An Alpine expression for a live value (a wizard step, an upload count):
// the indicator width and aria-valuenow follow it, so a block never needs
// a second hand-rolled track beside the server-rendered one.
'bind' => null,
// default (8px rounded track) | edge (a 2px square-ended strip that sits
// on the top edge of a panel or row while background work runs).
'size' => 'default',
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$value = max(0, min((float) $value, (float) $max));
$pct = $max > 0 ? round($value / $max * 100, 2) : 0;
// The fallbacks keep an older _styles.php (without neutral/primary) working.
$tones = $styles['progress']['tones'] + ['neutral' => 'bg-foreground', 'primary' => 'bg-primary'];
$tone = array_key_exists($tone, $tones) && $tone !== 'default' ? $tone : 'neutral';
// The fill colour is drawn from the root's data-tone (the group variants),
// so a live tone change on the root recolours the indicator, not the track.
$toneClass = $tones[$tone].' group-data-[tone=neutral]/progress:bg-foreground group-data-[tone=default]/progress:bg-foreground group-data-[tone=primary]/progress:bg-primary group-data-[tone=success]/progress:bg-success group-data-[tone=warning]/progress:bg-warning group-data-[tone=destructive]/progress:bg-destructive';
$animation = in_array($animation, ['none', 'striped', 'shimmer'], true) ? $animation : 'none';
$animated = ! $indeterminate && $animation !== 'none';
$size = $size === 'edge' ? 'edge' : 'default';
$edge = $size === 'edge';
// An edge strip is usually pinned by the caller (absolute inset-x-0
// top-0), so it only claims `relative` when no position class is given.
$positioned = preg_match('/(?:^|\s)!?(?:absolute|fixed|sticky)(?:\s|$)/', (string) $attributes->get('class', '')) === 1;
$rootClass = $edge
? 'group/progress w-full overflow-hidden bg-muted '.($positioned ? '' : 'relative ').($styles['progress']['sizes']['edge'] ?? 'h-0.5 rounded-none')
: 'group/progress relative h-2 w-full overflow-hidden rounded-full bg-muted';
$needsName = $label === null && ! $attributes->has('aria-label') && ! $attributes->has('aria-labelledby');
@endphp
<div
data-slot="progress"
role="progressbar"
aria-valuemin="0"
aria-valuemax="{{ $max }}"
@if ($label !== null) aria-label="{{ $label }}" @elseif ($needsName) aria-label="{{ __('Progress') }}" @endif
@unless ($indeterminate) aria-valuenow="{{ $value }}" @endunless
@if ($bind !== null && ! $indeterminate) x-bind:aria-valuenow="{{ $bind }}" @endif
data-state="{{ $indeterminate ? 'indeterminate' : 'determinate' }}"
data-tone="{{ $tone }}"
@if ($animated) data-animation="{{ $animation }}" @endif
@if ($edge) data-size="edge" @endif
{{ $attributes->merge(['class' => $rootClass]) }}
>
@if ($indeterminate)
{{-- Animated marquee bar (RTL-safe via inset-inline-start; see ui.css keyframes). --}}
<div
data-slot="progress-indicator"
@class([
'absolute inset-y-0 w-2/5',
'rounded-none' => $edge,
'rounded-full' => ! $edge,
'motion-safe:animate-[ui-progress-indeterminate_1.4s_ease-in-out_infinite] motion-reduce:w-full',
$toneClass,
])
></div>
@else
<div
data-slot="progress-indicator"
@class([
'h-full',
'rounded-none' => $edge,
'rounded-full' => ! $edge,
'transition-[width] duration-300 ease-out motion-reduce:transition-none',
$toneClass,
])
style="width: {{ $pct }}%;"
@if ($bind !== null) x-bind:style="'width: ' + Math.max(0, Math.min(100, ({{ $bind }}) / {{ (float) $max }} * 100)) + '%'" @endif
></div>
@endif
@if ($animated)
{{-- Striped / shimmer overlay. Colour comes from the surface token at low
opacity (hsl(var()) is the repo's safe in-<style> token form — it is
NOT an inline style, so it never trips the raw-colour guards). The
sweep is gated behind prefers-reduced-motion so it sits still for
reduced-motion users while keeping the texture. --}}
<style>
[data-slot="progress"][data-animation="striped"] [data-slot="progress-indicator"] {
background-image: repeating-linear-gradient(
45deg,
color-mix(in oklab, var(--background-color, hsl(var(--background))) calc(0.22 * 100%), transparent) 0 0.5rem,
transparent 0.5rem 1rem
);
background-size: 1rem 1rem;
}
[data-slot="progress"][data-animation="shimmer"] [data-slot="progress-indicator"] {
background-image: linear-gradient(
90deg,
transparent 0%,
color-mix(in oklab, var(--background-color, hsl(var(--background))) calc(0.45 * 100%), transparent) 50%,
transparent 100%
);
background-size: 200% 100%;
background-repeat: no-repeat;
}
@media (prefers-reduced-motion: no-preference) {
[data-slot="progress"][data-animation="striped"] [data-slot="progress-indicator"] {
animation: ui-progress-stripes 1s linear infinite;
}
[data-slot="progress"][data-animation="shimmer"] [data-slot="progress-indicator"] {
animation: ui-progress-shimmer 1.6s ease-in-out infinite;
}
}
@keyframes ui-progress-stripes {
from { background-position-x: 0; }
to { background-position-x: 1rem; }
}
@keyframes ui-progress-shimmer {
from { background-position-x: 200%; }
to { background-position-x: -200%; }
}
</style>
@endif
</div>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.5.1 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0