Components
Progress
Linear bar for task completion. Three thicknesses, optional label + value, indeterminate loop for unknown duration.
Preview#
Sync30%
Upload65%
Build90%
<ForenProgress size="sm" label="Sync" value={30} />
<ForenProgress size="md" label="Upload" value={65} />
<ForenProgress size="lg" label="Build" value={90} />Installation#
Same package as the rest of Foren.
bash
npm install @foren/uitsx
import { ForenProgress } from "@foren/ui";Usage#
Pass a number 0–100. Width animates on change.
tsx
<ForenProgress value={75} />
<ForenProgress label="Upload" value={progress} />
// Hide the auto value text
<ForenProgress value={42} hideValue />
// Custom value formatter
<ForenProgress value={3} max={10} valueText="3 of 10" />Source#
Drop-in source if you'd rather skip the package.
components/ui/foren-progress.tsx
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const trackVariants = cva(
"relative w-full overflow-hidden bg-[var(--color-foren-bg-track,var(--color-foren-bg-button-secondary-active))] rounded-full",
{
variants: {
size: {
sm: "h-1.5",
md: "h-2",
lg: "h-3",
},
},
defaultVariants: { size: "md" },
}
);
const labelVariants = cva(
"flex items-baseline justify-between gap-3 font-medium tracking-[0.01em]",
{
variants: {
size: {
sm: "text-[12px] leading-[18px] mb-1.5",
md: "text-[12px] leading-[18px] mb-1.5",
lg: "text-[14px] leading-[21px] mb-2",
},
},
defaultVariants: { size: "md" },
}
);
export interface ForenProgressProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
VariantProps<typeof trackVariants> {
/** 0–100. Ignored when `indeterminate` is true. */
value?: number;
/** Maximum value (defaults to 100). */
max?: number;
/** Show looping animation; value ignored. */
indeterminate?: boolean;
/** Label above the bar. */
label?: React.ReactNode;
/** Hide the percentage display. Defaults to false (visible). */
hideValue?: boolean;
/** Override the formatted value text. */
valueText?: React.ReactNode;
/** Custom fill color (defaults to Foren Sage 700). */
fillColor?: string;
}
export const ForenProgress = React.forwardRef<HTMLDivElement, ForenProgressProps>(
function ForenProgress(
{
className,
size = "md",
value = 0,
max = 100,
indeterminate = false,
label,
hideValue = false,
valueText,
fillColor = "var(--color-foren-brand-green)",
...props
},
ref
) {
const targetPct = Math.max(0, Math.min(100, (value / max) * 100));
const showHeader = label || (!hideValue && !indeterminate);
// Smoothly tween the displayed value so the text rolls instead of snapping.
// Bar width animates from the same source for perfect sync.
const [displayPct, setDisplayPct] = React.useState(targetPct);
React.useEffect(() => {
if (indeterminate) return;
const start = displayPct;
const end = targetPct;
if (start === end) return;
const duration = 600;
const t0 = performance.now();
const ease = (t: number) => 1 - Math.pow(1 - t, 3); // ease-out cubic
let raf = 0;
const tick = (now: number) => {
const t = Math.min(1, (now - t0) / duration);
setDisplayPct(start + (end - start) * ease(t));
if (t < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetPct, indeterminate]);
const rounded = Math.round(displayPct);
return (
<div ref={ref} className={cn("w-full", className)} {...props}>
{showHeader && (
<div className={labelVariants({ size })}>
<span className="text-[var(--color-foren-text-primary)] truncate">{label}</span>
{!hideValue && !indeterminate && (
<span className="text-[var(--color-foren-text-tertiary)] tabular-nums shrink-0">
{valueText ?? `${rounded}%`}
</span>
)}
</div>
)}
<div
role="progressbar"
aria-valuenow={indeterminate ? undefined : Math.round(targetPct)}
aria-valuemin={0}
aria-valuemax={100}
aria-valuetext={indeterminate ? "Loading" : `${Math.round(targetPct)}%`}
aria-label={
typeof label === "string" ? label : props["aria-label"] ?? "Progress"
}
className={trackVariants({ size })}
>
{indeterminate ? (
<span
aria-hidden
className="absolute inset-y-0 rounded-full"
style={{
backgroundColor: fillColor,
width: "40%",
animation: "forenProgressIndeterminate 1.4s cubic-bezier(0.4, 0, 0.6, 1) infinite",
}}
/>
) : (
<span
aria-hidden
className="block h-full rounded-full"
style={{ width: `${displayPct}%`, backgroundColor: fillColor }}
/>
)}
</div>
</div>
);
}
);
Sizes#
Three thicknesses — sm 6px, md 8px (default), lg 12px.
Size sm60%
Size md60%
Size lg60%
<ForenProgress size="sm" value={60} />
<ForenProgress size="md" value={60} />
<ForenProgress size="lg" value={60} />With label + value#
Optional. Drop label, hide value, or override the formatted text.
Label + auto value45%
Hidden value
Custom text3 of 10 files
45%
<ForenProgress label="Label + auto value" value={45} />
<ForenProgress label="Hidden value" value={45} hideValue />
<ForenProgress label="Custom text" value={3} max={10} valueText="3 of 10 files" />
<ForenProgress value={45} aria-label="Bare bar" />Value matrix#
Same size, every 10% stop.
0%0%
10%10%
20%20%
30%30%
40%40%
50%50%
60%60%
70%70%
80%80%
90%90%
100%100%
{[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map(v => (
<ForenProgress key={v} label={`${v}%`} value={v} />
))}Indeterminate#
Looping bar — for unknown duration tasks. Skips the value text.
Fetching tokens
<ForenProgress label="Fetching tokens" indeterminate />
<ForenProgress size="lg" indeterminate />Controlled#
Use buttons to step the value — bar width transitions on change.
Upload40%
const [v, setV] = useState(40);
<ForenProgress label="Upload" value={v} />
<button onClick={() => setV((p) => p + 10)}>+10</button>Auto-loop#
Self-incrementing demo.
Auto-incrementing0%
const [v, setV] = useState(0);
useEffect(() => {
const t = setInterval(() => setV((p) => (p >= 100 ? 0 : p + 5)), 400);
return () => clearInterval(t);
}, []);
<ForenProgress label="Auto-incrementing" value={v} />API#
PropTypeDefault
valuenumber (0–100)
maxnumber
size"sm" | "md" | "lg"
labelReactNode
hideValueboolean
valueTextReactNode (override)
indeterminateboolean
fillColorstring (CSS color)
…restHTMLAttributes<HTMLDivElement>