Progress
A determinate or indeterminate progress bar.
A linear progress bar. Android draws the MD3 4px track with the primary indicator and a dual-segment indeterminate animation; iOS draws the thin UIProgressView-style track. Set value for determinate progress, or indeterminate for continuous activity.
Installation
npx touchcn add progress
Usage
<tcn-progress [value]="62" />
<tcn-progress indeterminate />import { TcnProgress } from '@/components/ui/progress';
<TcnProgress value={62} />
<TcnProgress indeterminate /><script setup lang="ts">
import { TcnProgress } from '@/components/ui/progress';
</script>
<template>
<TcnProgress :value="62" />
<TcnProgress indeterminate />
</template>The bar carries role="progressbar" with aria-valuemin / aria-valuemax / aria-valuenow; the value attributes are omitted while indeterminate.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value |
number |
0 |
Completion 0–100 (determinate). |
indeterminate |
boolean |
false |
Run the continuous animation. |
Source · Angular
export * from './tcn-progress';import { booleanAttribute, Component, computed, input, numberAttribute } from '@angular/core';
/**
* Progress bar. Determinate mode fills the indicator to `value` (0–100),
* value-driven through the `--tcn-progress-value` custom property so no
* per-frame JS styling is needed; `indeterminate` runs the MD3 dual-segment
* sweep (a single travelling segment on iOS). MD3 draws a 4px track with the
* primary indicator; iOS draws the thin UIProgressView-style track. Carries
* `role="progressbar"` with `aria-value*` (omitted while indeterminate).
*/
@Component({
selector: 'tcn-progress',
template: `
<div class="tcn-progress-track">
<div class="tcn-progress-indicator" [style.--tcn-progress-value]="fill()"></div>
<div class="tcn-progress-seg tcn-progress-seg-1" aria-hidden="true"></div>
<div class="tcn-progress-seg tcn-progress-seg-2" aria-hidden="true"></div>
</div>
`,
host: {
role: 'progressbar',
class: 'tcn-progress',
'[attr.data-indeterminate]': 'indeterminate() || null',
'[attr.aria-valuemin]': 'indeterminate() ? null : 0',
'[attr.aria-valuemax]': 'indeterminate() ? null : 100',
'[attr.aria-valuenow]': 'indeterminate() ? null : clamped()',
},
})
export class TcnProgress {
readonly value = input(0, { transform: numberAttribute });
readonly indeterminate = input(false, { transform: booleanAttribute });
protected readonly clamped = computed(() => Math.max(0, Math.min(100, this.value())));
protected readonly fill = computed(() => `${this.clamped()}%`);
}Source · React
export * from './tcn-progress';import { forwardRef } from 'react';
import type { ComponentPropsWithoutRef, CSSProperties } from 'react';
import { cn } from '@touchcn/core';
export interface TcnProgressProps extends Omit<ComponentPropsWithoutRef<'div'>, 'role'> {
/** Completion 0–100 (determinate mode). */
value?: number;
/** Run the continuous indeterminate animation instead of a fixed fill. */
indeterminate?: boolean;
}
/**
* Progress bar. Determinate mode fills the indicator to `value` (0–100),
* value-driven through the `--tcn-progress-value` custom property so no
* per-frame JS styling is needed; `indeterminate` runs the MD3 dual-segment
* sweep (a single travelling segment on iOS). MD3 draws a 4px track with the
* primary indicator; iOS draws the thin UIProgressView-style track. Emits the
* same `tcn-progress*` markup as the Angular component with `role="progressbar"`
* and `aria-value*` (omitted while indeterminate).
*/
export const TcnProgress = forwardRef<HTMLDivElement, TcnProgressProps>(function TcnProgress(
{ value = 0, indeterminate = false, className, ...props },
ref,
) {
const clamped = Math.max(0, Math.min(100, value));
return (
<div
ref={ref}
role="progressbar"
className={cn('tcn-progress', className)}
data-indeterminate={indeterminate || undefined}
aria-valuemin={indeterminate ? undefined : 0}
aria-valuemax={indeterminate ? undefined : 100}
aria-valuenow={indeterminate ? undefined : clamped}
{...props}
>
<div className="tcn-progress-track">
<div className="tcn-progress-indicator" style={{ '--tcn-progress-value': `${clamped}%` } as CSSProperties} />
<div className="tcn-progress-seg tcn-progress-seg-1" aria-hidden="true" />
<div className="tcn-progress-seg tcn-progress-seg-2" aria-hidden="true" />
</div>
</div>
);
});Source · Vue
<script setup lang="ts">
import { computed } from 'vue';
import type { CSSProperties } from 'vue';
/**
* Progress bar. Determinate mode fills the indicator to `value` (0–100),
* value-driven through the `--tcn-progress-value` custom property so no
* per-frame JS styling is needed; `indeterminate` runs the MD3 dual-segment
* sweep (a single travelling segment on iOS). MD3 draws a 4px track with the
* primary indicator; iOS draws the thin UIProgressView-style track. Emits the
* same `tcn-progress*` markup as the Angular / React component with
* `role="progressbar"` and `aria-value*` (omitted while indeterminate).
*/
const props = withDefaults(defineProps<{ value?: number; indeterminate?: boolean }>(), {
value: 0,
indeterminate: false,
});
const clamped = computed(() => Math.max(0, Math.min(100, props.value)));
const indicatorStyle = computed<CSSProperties>(
() => ({ '--tcn-progress-value': `${clamped.value}%` }) as CSSProperties,
);
</script>
<template>
<div
role="progressbar"
class="tcn-progress"
:data-indeterminate="indeterminate || undefined"
:aria-valuemin="indeterminate ? undefined : 0"
:aria-valuemax="indeterminate ? undefined : 100"
:aria-valuenow="indeterminate ? undefined : clamped"
>
<div class="tcn-progress-track">
<div class="tcn-progress-indicator" :style="indicatorStyle" />
<div class="tcn-progress-seg tcn-progress-seg-1" aria-hidden="true" />
<div class="tcn-progress-seg tcn-progress-seg-2" aria-hidden="true" />
</div>
</div>
</template>export { default as TcnProgress } from './TcnProgress.vue';