Progress Linear
A shadcn-style progress linear component built with Ark UI primitives.
50%
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearDefault = () => (
<div className="w-full max-w-md">
<ProgressLinear defaultValue={50} showValueText />
</div>
);
export default ProgressLinearDefault;
Installation
npx shadcn@latest add @ark-cn/progress-linearInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/progress-linear.tsx
"use client";
import {
Progress as ProgressPrimitive,
useProgress,
useProgressContext,
} from "@ark-ui/react/progress";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export { useProgress, useProgressContext };
export type ProgressLinearRootProps = ProgressPrimitive.RootProps;
export const ProgressLinearRoot = ({
className,
...props
}: ProgressLinearRootProps) => (
<ProgressPrimitive.Root
className={cn(
"group/progress-linear flex w-full flex-col gap-2",
className,
)}
data-slot="progress-linear"
{...props}
/>
);
export type ProgressLinearRootProviderProps =
ProgressPrimitive.RootProviderProps;
export const ProgressLinearRootProvider = ({
className,
...props
}: ProgressLinearRootProviderProps) => (
<ProgressPrimitive.RootProvider
className={cn(
"group/progress-linear flex w-full flex-col gap-2",
className,
)}
data-slot="progress-linear-root-provider"
{...props}
/>
);
export type ProgressLinearLabelProps = ProgressPrimitive.LabelProps;
export const ProgressLinearLabel = ({
className,
...props
}: ProgressLinearLabelProps) => (
<ProgressPrimitive.Label
className={cn(
"font-medium text-foreground text-sm leading-none",
className,
)}
data-slot="progress-linear-label"
{...props}
/>
);
export type ProgressLinearValueTextProps = ProgressPrimitive.ValueTextProps;
export const ProgressLinearValueText = ({
className,
...props
}: ProgressLinearValueTextProps) => (
<ProgressPrimitive.ValueText
className={cn("tabular-nums text-foreground text-sm", className)}
data-slot="progress-linear-value-text"
{...props}
/>
);
export type ProgressLinearTrackProps = ProgressPrimitive.TrackProps;
export const ProgressLinearTrack = ({
className,
...props
}: ProgressLinearTrackProps) => (
<ProgressPrimitive.Track
className={cn(
"relative w-full overflow-hidden rounded-full bg-muted",
"data-[orientation=vertical]:h-44 data-[orientation=vertical]:w-2.5 data-[orientation=vertical]:shrink-0",
className,
)}
data-slot="progress-linear-track"
{...props}
/>
);
export type ProgressLinearRangeProps = ProgressPrimitive.RangeProps;
export const ProgressLinearRange = ({
className,
...props
}: ProgressLinearRangeProps) => (
<ProgressPrimitive.Range
className={cn(
"rounded-full bg-primary transition-[width,height] duration-300 ease-out",
"data-[orientation=horizontal]:h-full",
"data-[orientation=vertical]:w-full",
"data-[state=indeterminate]:min-h-[30%] data-[state=indeterminate]:min-w-[30%] data-[state=indeterminate]:motion-safe:animate-pulse",
className,
)}
data-slot="progress-linear-range"
{...props}
/>
);
export type ProgressLinearViewProps = ProgressPrimitive.ViewProps;
export const ProgressLinearView = ({
className,
...props
}: ProgressLinearViewProps) => (
<ProgressPrimitive.View
className={cn(className)}
data-slot="progress-linear-view"
{...props}
/>
);
export type ProgressLinearContextProps = ProgressPrimitive.ContextProps;
export const ProgressLinearContext = ({
children,
}: ProgressLinearContextProps) => (
<ProgressPrimitive.Context>{children}</ProgressPrimitive.Context>
);
const trackSizeClass = {
lg: "h-3",
md: "h-2",
sm: "h-1",
} as const;
export type ProgressLinearProps = Omit<
ProgressLinearRootProps,
"children" | "className"
> & {
children?: ReactNode;
className?: string;
label?: ReactNode;
rangeClassName?: string;
showValueText?: boolean;
size?: keyof typeof trackSizeClass;
trackClassName?: string;
valueTextClassName?: string;
};
export const ProgressLinear = ({
children,
className,
label,
rangeClassName,
showValueText = false,
size = "md",
trackClassName,
valueTextClassName,
orientation,
...rootProps
}: ProgressLinearProps) => {
const trackSize =
orientation === "vertical" ? undefined : trackSizeClass[size];
return (
<ProgressLinearRoot
className={className}
orientation={orientation}
{...rootProps}
>
{children ?? (
<>
{label != null || showValueText ? (
<div className="flex items-center justify-between gap-2">
{label != null ? (
<ProgressLinearLabel className="truncate">
{label}
</ProgressLinearLabel>
) : (
<span aria-hidden className="min-w-0 shrink" />
)}
{showValueText ? (
<ProgressLinearValueText
className={cn(
"shrink-0 text-muted-foreground text-xs",
valueTextClassName,
)}
/>
) : null}
</div>
) : null}
<ProgressLinearTrack className={cn(trackSize, trackClassName)}>
<ProgressLinearRange className={rangeClassName} />
</ProgressLinearTrack>
</>
)}
</ProgressLinearRoot>
);
};
Update import aliases to match your project setup.
Usage
import * as ProgressLinear from "@/components/ui/progress-linear"Read exported parts in src/components/ui/progress-linear.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Default
50%
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearDefault = () => (
<div className="w-full max-w-md">
<ProgressLinear defaultValue={50} showValueText />
</div>
);
export default ProgressLinearDefault;
Min and max
1
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearMinMax = () => (
<div className="w-full max-w-md">
<ProgressLinear
defaultValue={20}
formatOptions={{ maximumFractionDigits: 0, style: "decimal" }}
max={30}
min={10}
showValueText
/>
</div>
);
export default ProgressLinearMinMax;
Indeterminate
Loading
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearIndeterminate = () => (
<div className="w-full max-w-md">
<ProgressLinear label="Loading" value={null} />
</div>
);
export default ProgressLinearIndeterminate;
Value text translations
72%
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearValueText = () => (
<div className="w-full max-w-md">
<ProgressLinear
defaultValue={72}
showValueText
translations={{
value: (d) =>
d.value == null
? "Loading"
: `${Math.round(d.percent)}% complete (${d.value} of ${d.max})`,
}}
/>
</div>
);
export default ProgressLinearValueText;
Vertical
Upload
55%
import {
ProgressLinearLabel,
ProgressLinearRange,
ProgressLinearRoot,
ProgressLinearTrack,
ProgressLinearValueText,
} from "@/components/ui/progress-linear";
const ProgressLinearVertical = () => (
<div className="flex items-end gap-4">
<ProgressLinearRoot
className="max-w-40 flex-row items-end gap-3"
defaultValue={55}
orientation="vertical"
>
<ProgressLinearLabel className="whitespace-nowrap pb-2 text-xs">
Upload
</ProgressLinearLabel>
<ProgressLinearTrack>
<ProgressLinearRange />
</ProgressLinearTrack>
<ProgressLinearValueText className="pb-2 text-muted-foreground text-xs" />
</ProgressLinearRoot>
</div>
);
export default ProgressLinearVertical;
Root provider
useProgress + RootProvider - 28
External controlimport { Button } from "@/components/ui/button";
import {
ProgressLinearLabel,
ProgressLinearRange,
ProgressLinearRootProvider as ProgressLinearRootProviderPrimitive,
ProgressLinearTrack,
useProgress,
} from "@/components/ui/progress-linear";
const ProgressLinearRootProvider = () => {
const store = useProgress({ defaultValue: 28 });
return (
<ProgressLinearRootProviderPrimitive className="max-w-md" value={store}>
<p className="text-muted-foreground text-xs">
useProgress + RootProvider -{" "}
<span className="font-medium text-foreground tabular-nums">
{store.value ?? "-"}
</span>
</p>
<ProgressLinearLabel>External control</ProgressLinearLabel>
<ProgressLinearTrack className="h-2">
<ProgressLinearRange />
</ProgressLinearTrack>
<div className="flex flex-wrap gap-2">
<Button
onClick={() => store.setValue(Math.max(0, (store.value ?? 0) - 12))}
size="sm"
type="button"
variant="outline"
>
-12
</Button>
<Button
onClick={() => store.setValue(Math.min(100, (store.value ?? 0) + 12))}
size="sm"
type="button"
variant="outline"
>
+12
</Button>
<Button
onClick={() => store.setToMin()}
size="sm"
type="button"
variant="secondary"
>
Min
</Button>
<Button
onClick={() => store.setToMax()}
size="sm"
type="button"
variant="secondary"
>
Max
</Button>
</div>
</ProgressLinearRootProviderPrimitive>
);
};
export default ProgressLinearRootProvider;
Composed anatomy
Anatomy composition
58%
import {
ProgressLinearLabel,
ProgressLinearRange,
ProgressLinearRoot,
ProgressLinearTrack,
ProgressLinearValueText,
} from "@/components/ui/progress-linear";
const ProgressLinearComposed = () => (
<ProgressLinearRoot className="max-w-md" defaultValue={58}>
<ProgressLinearLabel>Anatomy composition</ProgressLinearLabel>
<ProgressLinearTrack className="h-2">
<ProgressLinearRange />
</ProgressLinearTrack>
<ProgressLinearValueText className="text-muted-foreground text-xs" />
</ProgressLinearRoot>
);
export default ProgressLinearComposed;
Controlled
42%
import { useState } from "react";
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearControlled = () => {
const [value, setValue] = useState(42);
return (
<div className="flex w-full max-w-md flex-col gap-3">
<ProgressLinear
onValueChange={(d) => setValue(d.value ?? 0)}
showValueText
value={value}
/>
<input
aria-label="Linear progress value"
className="w-full accent-primary"
max={100}
min={0}
onChange={(e) => setValue(Number(e.target.value))}
type="range"
value={value}
/>
</div>
);
};
export default ProgressLinearControlled;
View states
Still working...
Finished
Indeterminate
import {
ProgressLinearRange,
ProgressLinearRoot,
ProgressLinearTrack,
ProgressLinearView,
} from "@/components/ui/progress-linear";
const ProgressLinearViewDemo = () => (
<ProgressLinearRoot className="max-w-md" defaultValue={75}>
<div className="flex min-h-10 flex-col gap-1">
<ProgressLinearView state="loading">
<p className="text-muted-foreground text-xs">Still working...</p>
</ProgressLinearView>
<ProgressLinearView state="complete">
<p className="text-emerald-600 text-xs">Finished</p>
</ProgressLinearView>
<ProgressLinearView state="indeterminate">
<p className="text-muted-foreground text-xs">Indeterminate</p>
</ProgressLinearView>
</div>
<ProgressLinearTrack className="mt-1 h-2">
<ProgressLinearRange />
</ProgressLinearTrack>
</ProgressLinearRoot>
);
export default ProgressLinearViewDemo;
Context
48% · determinate
import {
ProgressLinearContext,
ProgressLinearRange,
ProgressLinearRoot,
ProgressLinearTrack,
} from "@/components/ui/progress-linear";
const ProgressLinearContextDemo = () => (
<ProgressLinearRoot className="max-w-md" defaultValue={48}>
<ProgressLinearContext>
{(ctx) => (
<p className="text-muted-foreground text-xs">
<span className="font-medium text-foreground tabular-nums">
{ctx.percentAsString || `${Math.round(ctx.percent)}%`}
</span>
{" · "}
{ctx.indeterminate ? "indeterminate" : "determinate"}
</p>
)}
</ProgressLinearContext>
<ProgressLinearTrack className="h-2">
<ProgressLinearRange />
</ProgressLinearTrack>
</ProgressLinearRoot>
);
export default ProgressLinearContextDemo;
With field
66%
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearWithField = () => (
<Field className="max-w-md">
<FieldLabel>Task progress</FieldLabel>
<ProgressLinear defaultValue={66} showValueText />
<FieldDescription>
Field label and helper; linear bar uses Root, Track, and Range.
</FieldDescription>
</Field>
);
export default ProgressLinearWithField;
Sizes
Small30%
Medium50%
Large80%
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearSizes = () => (
<div className="flex w-full max-w-md flex-col gap-4">
<ProgressLinear defaultValue={30} label="Small" showValueText size="sm" />
<ProgressLinear defaultValue={50} label="Medium" showValueText size="md" />
<ProgressLinear defaultValue={80} label="Large" showValueText size="lg" />
</div>
);
export default ProgressLinearSizes;
Complete
Complete100%
import { ProgressLinear } from "@/components/ui/progress-linear";
const ProgressLinearComplete = () => (
<div className="w-full max-w-md">
<ProgressLinear defaultValue={100} label="Complete" showValueText />
</div>
);
export default ProgressLinearComplete;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
ProgressLinear
| Prop | Type | Description |
|---|---|---|
| label? | ReactNode | Optional label above the track. |
| showValueText? | boolean | Shows the value text on the right. |
| size? | "sm" | "md" | "lg" | Track height tier. |
| trackClassName? | string | Class on the track element. |
| rangeClassName? | string | Class on the range fill. |
| valueTextClassName? | string | Class on the value text. |
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.