suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Progress

A horizontal bar that shows the completion of a task.

Progress is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

import { Progress } from "@/components/ui/progress"

export default function ProgressDemo() {
  return (
    <div className="w-80">
      <Progress value={60} aria-label="Upload progress" />
    </div>
  )
}

Progress wraps the progress primitive of the chosen base. Progress is the root and always renders a ProgressTrack containing a ProgressIndicator after its children. Add a ProgressLabel to name the bar and a ProgressValue to show the formatted value; they sit on one row above the track.

Use Progress for determinate or indeterminate task progress.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/progress

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add progress

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react
  1. Copy the source into components/ui/progress.tsx:
  1. Update the import paths to match your project.

Usage

import { Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue } 
<div

Pass value as a number between min and max (by default 0 and 100). Set value={null} for an indeterminate state when the amount of work is unknown.

The track and indicator are rendered for you, so ProgressTrack and ProgressIndicator are not needed inside Progress children. ProgressValue shows the formatted value, a percentage by default.

Examples

With label

A bar with a label and its current value shown above the track.

Complete

A labelled bar at 100.

Indeterminate

A labelled bar with value={null} for work of unknown length.

API Reference

Anatomy3 parts
<Progress>
<ProgressLabel />
<ProgressValue />
</Progress>
01

Progress

The root that holds the value and renders the track and indicator.

Also accepts the props of the primitive part it wraps.

02

ProgressTrack

The full-width rail behind the indicator.

Also accepts the props of the primitive part it wraps.

03

ProgressIndicator

The filled portion of the track that reflects the value.

Also accepts the props of the primitive part it wraps.

04

ProgressLabel

Text that names the progress bar.

Also accepts the props of the primitive part it wraps.

05

ProgressValue

The formatted current value, aligned to the end of the row.

Also accepts the props of the primitive part it wraps.

Accessibility

  • The root has the progressbar role with aria-valuenow reflecting the value.
  • ProgressLabel provides the accessible name. Without it, pass aria-label.
  • When value is null, aria-valuenow is omitted to signal an indeterminate state.

Notes

  • Styling hooks: data-slot values progress, progress-track, progress-indicator, progress-label and progress-value.
  • The track uses muted and the indicator uses primary; the indicator animates width changes.

Progress renders a thin track with an indicator whose width follows value. The width animates with a Reanimated spring, clamped between 0 and 100 percent. Any children, such as ProgressLabel and ProgressValue, are placed in a row above the bar.

Use it for uploads, downloads and other tasks with a known amount of work.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/progress

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add progress
$ pnpm dlx @esuiss/ui@latest add progress --platform swift
NextQR ScannerPreviousPopover

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • With label
    • Complete
    • Indeterminate
  4. 04API Reference
    • Progress
    • ProgressTrack
    • ProgressIndicator
    • ProgressLabel
    • ProgressValue
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • With label
    • Complete
    • Values
  10. 10API Reference
    • Progress
    • ProgressTrack
    • ProgressIndicator
    • ProgressLabel
    • ProgressValue
  11. 11Accessibility
  12. 12Notes
from
"@/components/ui/progress"
className
=
"w-80"
>
<Progress value={60} aria-label="Upload progress" />
</div>
$ pnpm dlx @esuiss/ui@latest add progress --platform kotlin

Manual

  1. Install the dependencies:
npx expo install cn react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: the primitives components/primitives/progress.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/progress.tsx:

uniwind/components/ui/progress.tsx
import { cn } from "cn"
import * as React from "react"
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from "react-native-reanimated"
import * as ProgressPrimitive from "@/components/primitives/progress"
import { resolveClassName } from "@/components/primitives/use-render"

function Progress({
  className,
  children,
  trackClassName,
  indicatorClassName,
  ...props
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: the primitives components/primitives/progress.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/progress.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: the primitives components/primitives/progress.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/progress.tsx:

  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Copy the theme files every view uses:

swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/Progress.swift
import SwiftUI

public struct BarProgressViewStyle: ProgressViewStyle {
  public init() {}

  public func makeBody(configuration: Configuration) -> some View {
    BarProgress(configuration: configuration)
  }
}

private struct BarProgress: View {
  let configuration: ProgressViewStyleConfiguration

  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  var body: some View {
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source:
kotlin/components/Progress.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue } from "@/components/ui/progress"
<ProgressDemo />

Set value between min and max (0 and 100 by default). Pass null when the progress is unknown.

Add a ProgressLabel and a ProgressValue as children to show a caption and the formatted value; the value is pushed to the end of the row. Format the value with format and locale.

Style the bar with trackClassName and indicatorClassName. ProgressTrack and ProgressIndicator are also exported for custom layouts.

Examples

With label

A bar with a label and a live percentage value.

iOS
Progress (with label) on iOS in light modeProgress (with label) on iOS in dark mode
import * as React from "react"
import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress"

function WithLabelDemo() {
  const [value, setValue] = React.useState(0)
  React.useEffect(() => {
    const timer = setInterval(() => {
      setValue((current) => (current >= 100 ? 0 : current + 10))
    }, 600)
    return () => clearInterval(timer)
  }, [])
  return (
    <Progress value={value}>
      <ProgressLabel>Uploading</ProgressLabel>
      <ProgressValue />
    </Progress>
  )
}

export function ProgressWithLabelExample() {
  return (
    <WithLabelDemo />
  )
}
import EsuissUI
import SwiftUI

struct ProgressWithLabel: View {
  @State private var value = 0

  var body: some View {
    ProgressView(value: Double(value), total: 100) {
      Text("Uploading")
    } currentValueLabel: {
      Text(verbatim: "\(value)%")
    }
    .progressViewStyle(.bar)
    .task {
      while !Task.isCancelled {
        try? await Task.sleep(for: .milliseconds(600))
        value = value >= 100 ? 0 : value + 10
      }
    }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.esuiss.ui.components.Progress
import kotlinx.coroutines.delay

@Composable
fun ProgressWithLabel() {
    var value by rememberSaveable { mutableFloatStateOf(0f) }
    LaunchedEffect(Unit) {
        while (true) {
            delay(600)
            value = if (value >= 100f) 0f else value + 10f
        }
    }
    Progress(value, Modifier.fillMaxWidth(), label = "Uploading", showValue = true)
}

Complete

A bar at 100 percent.

iOS
Progress (complete) on iOS in light modeProgress (complete) on iOS in dark mode
import { Progress } from "@/components/ui/progress"

export function ProgressCompleteExample() {
  return (
    <Progress value={100} />
  )
}
import EsuissUI
import SwiftUI

struct ProgressCompleteExample: View {
  var body: some View {
    ProgressView(value: 100, total: 100)
      .progressViewStyle(.bar)
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.esuiss.ui.components.Progress

@Composable
fun ProgressCompleteExample() {
    Progress(100f, Modifier.fillMaxWidth())
}

Values

Bars at 0, 25, 50, 75 and 100 percent, the last with a custom indicator color.

iOS
Progress (values) on iOS in light modeProgress (values) on iOS in dark mode
import { View } from "react-native"
import { Progress } from "@/components/ui/progress"

export function ProgressValuesExample() {
  return (
    <View className="w-full gap-4">
      <Progress value={0} />
      <Progress value={25} />
      <Progress value={50} />
      <Progress value={75} />
      <Progress value={100} indicatorClassName="bg-green-500" />
    </View>
  )
}
import EsuissUI
import SwiftUI

struct ProgressValuesExample: View {
  var body: some View {
    VStack(spacing: 16) {
      ForEach([0.0, 25, 50, 75, 100], id: \.self) { value in
        ProgressView(value: value, total: 100)
          .progressViewStyle(.bar)
      }
    }
    .frame(maxWidth: .infinity)
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Progress

@Composable
fun ProgressValues() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        listOf(0f, 25f, 50f, 75f, 100f).forEach { Progress(it, Modifier.fillMaxWidth()) }
    }
}

API Reference

01

Progress

Root that holds the value and renders the track and indicator.

PropTypeDefaultDescription
trackClassNamestring-Extra classes for the track.
indicatorClassNamestring-Extra classes for the indicator.

Also accepts the props of React.ComponentProps<typeof ProgressPrimitive.Root>.

02

ProgressTrack

Background bar.

Also accepts the props of React.ComponentProps<typeof ProgressPrimitive.Track>.

03

ProgressIndicator

Animated fill that shows the current value.

Also accepts the props of React.ComponentProps<typeof ProgressPrimitive.Indicator>.

04

ProgressLabel

Caption text above the bar.

Also accepts the props of React.ComponentProps<typeof ProgressPrimitive.Label>.

05

ProgressValue

Formatted value text, aligned to the end.

Also accepts the props of React.ComponentProps<typeof ProgressPrimitive.Value>.

Accessibility

  • The root has the progressbar role and reports min, max, the current value and a value text, so VoiceOver and TalkBack announce the progress.
  • Use getAccessibilityValueText to customize the announced text.

Notes

  • The track uses bg-muted and the indicator bg-primary.
  • Label and value text use text-foreground and text-muted-foreground, with tabular numbers for the value.
  • Light and dark colors follow the system appearance.
iOS
Progress on iOS in light modeProgress on iOS in dark mode
import * as React from "react"
import { Progress } from "@/components/ui/progress"

function ProgressDemo() {
  const [value, setValue] = React.useState(13)
  React.useEffect(() => {
    const timer = setTimeout(() => setValue(66), 500)
    return () => clearTimeout(timer)
  }, [])
  return <Progress value={value} />
}

export function ProgressExample() {
  return (
    <ProgressDemo />
  )
}
import EsuissUI
import SwiftUI

struct ProgressDelayed: View {
  @State private var value = 13.0

  var body: some View {
    ProgressView(value: value, total: 100)
      .progressViewStyle(.bar)
      .task {
        try? await Task.sleep(for: .milliseconds(500))
        value = 66
      }
  }
}
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.esuiss.ui.components.Progress
import kotlinx.coroutines.delay

@Composable
fun ProgressDefault() {
    var value by rememberSaveable { mutableFloatStateOf(13f) }
    LaunchedEffect(Unit) {
        delay(500)
        value = 66f
    }
    Progress(value, Modifier.fillMaxWidth())
}
components/ui/progress.tsx
"use client"

import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
import { cn } from "@/lib/utils"

function Progress({
  className,
  children,
  value,
  ...props
}: ProgressPrimitive.Root.Props) {
  return (
    <ProgressPrimitive.Root
      value={value}
      data-slot="progress"
      className={cn("flex flex-wrap gap-3", className)}
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export default function ProgressWithLabel() {
  return (
    <div className="w-80">
      <Progress value={42}>
        <ProgressLabel>Uploading files</ProgressLabel>
        <ProgressValue />
      </Progress>
    </div>
  )
}
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export default function ProgressComplete() {
  return (
    <div className="w-80">
      <Progress value={100}>
        <ProgressLabel>Uploading files</ProgressLabel>
        <ProgressValue />
      </Progress>
    </div>
  )
}
import { Progress, ProgressLabel } from "@/components/ui/progress"

export default function ProgressIndeterminate() {
  return (
    <div className="w-80">
      <Progress value={null}>
        <ProgressLabel>Preparing export</ProgressLabel>
      </Progress>
    </div>
  )
}
nativewind/components/ui/progress.tsx
import { cn } from "cn"
import * as React from "react"
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from "react-native-reanimated"
import * as ProgressPrimitive from "@/components/primitives/progress"
import { resolveClassName } from "@/components/primitives/use-render"

function Progress({
  className,
  children,
  trackClassName,
  indicatorClassName,
  ...props
stylesheet/components/ui/progress.tsx
import * as React from "react"
import { type StyleProp, StyleSheet, type ViewStyle } from "react-native"
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from "react-native-reanimated"
import * as ProgressPrimitive from "@/components/primitives/progress"
import { fontSize, radius, useTheme } from "@/components/ui/theme"

const styles = StyleSheet.create({
  root: {
    width: "100%",
    flexDirection: "row",
    flexWrap: "wrap",
    gap: 12,