Skip to content
Brok UI

Loading…

No results

Progress

Open source Core

Displays a determinate progress indicator with an optional threshold tone and a striped or shimmer animated fill.

Version
v1.5.1
Stability
stable
License
MIT
Related
Onboard Card
Spinner
Circular Progress

Preview

Uploading… (60%)

Working… (indeterminate)

Tone
Size
previews.components.progress.default.blade.php 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>

Tone options

Neutral Current
Primary Current
Default Current
Success Current
Warning Current
Destructive Current

Size options

Default Current
Edge Current

Installation

terminal
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.

  • blade 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.

progress.md
# 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

edge.blade.php Blade
{{-- 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>
live.blade.php Blade
{{-- 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>
tones.blade.php Blade
<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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

progress progress-indicator

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

Progress and sequence

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
root progress-indicator
Theming hooks
progress

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, 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 under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<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.

resources/views/components/ui/progress.blade.php Blade
@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