diff --git a/apps/www/src/content/docs/components/progress/demo.ts b/apps/www/src/content/docs/components/progress/demo.ts
new file mode 100644
index 00000000..314a008a
--- /dev/null
+++ b/apps/www/src/content/docs/components/progress/demo.ts
@@ -0,0 +1,160 @@
+'use client';
+
+import { getPropsString } from '@/lib/utils';
+
+export const getCode = (props: any) => {
+ return ``;
+};
+
+export const playground = {
+ type: 'playground',
+ controls: {
+ value: { type: 'number', initialValue: 40, min: 0, max: 100 },
+ variant: {
+ type: 'select',
+ initialValue: 'linear',
+ options: ['linear', 'circular']
+ },
+ min: { type: 'number', defaultValue: 0, min: 0, max: 99 },
+ max: { type: 'number', defaultValue: 100, min: 1, max: 100 }
+ },
+ getCode
+};
+
+export const directUsageDemo = {
+ type: 'code',
+ code: `
+
+
+
+`
+};
+
+export const variantDemo = {
+ type: 'code',
+ tabs: [
+ {
+ name: 'Linear',
+ code: `
+
+`
+ },
+ {
+ name: 'Circular',
+ code: `
+
+
+
+`
+ }
+ ]
+};
+
+export const customizationDemo = {
+ type: 'code',
+ tabs: [
+ {
+ name: 'Linear',
+ code: `
+
+
+
+`
+ },
+ {
+ name: 'Circular',
+ code: `
+
+
+
+
+`
+ }
+ ]
+};
+
+export const animatedDemo = {
+ type: 'code',
+ code: `function AnimatedProgress() {
+ const [value, setValue] = React.useState(0);
+
+ React.useEffect(() => {
+ const interval = setInterval(() => {
+ setValue((current) => {
+ if (current >= 100) return 0;
+ return Math.min(100, Math.round(current + Math.random() * 25));
+ });
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+
+
+
+ );
+}`
+};
+
+export const withLabelsDemo = {
+ type: 'code',
+ code: `
+
+
+`
+};
diff --git a/apps/www/src/content/docs/components/progress/index.mdx b/apps/www/src/content/docs/components/progress/index.mdx
new file mode 100644
index 00000000..e25b2e8d
--- /dev/null
+++ b/apps/www/src/content/docs/components/progress/index.mdx
@@ -0,0 +1,93 @@
+---
+title: Progress
+description: Displays the completion progress of a task.
+source: packages/raystack/components/progress
+tag: new
+---
+
+import { playground, directUsageDemo, variantDemo, customizationDemo, animatedDemo, withLabelsDemo } from "./demo.ts";
+
+
+
+## Anatomy
+
+Import and assemble the component:
+
+```tsx
+import { Progress } from '@raystack/apsara'
+
+{/* Direct usage — renders track automatically */}
+
+
+{/* Composable usage */}
+
+```
+
+## API Reference
+
+### Root
+
+The main container for the progress. Renders a default track when no children are provided. Set `value` to `null` for an indeterminate state.
+
+
+
+### Label
+
+Displays a label for the progress.
+
+
+
+### Value
+
+Displays the formatted current value as a percentage.
+
+
+
+### Track
+
+Contains the indicator that visualizes the current value.
+
+
+
+## Examples
+
+### Variant
+
+The progress supports `linear` and `circular` variants.
+
+
+
+### Direct Usage
+
+The simplest way to use the progress. When no children are provided, it renders the track automatically.
+
+
+
+### Customization
+
+Customize the track for both variants. For linear, use `height` on the track. For circular, use `width`/`height` on the track to control the overall size and `--rs-progress-track-size` to control the stroke thickness.
+
+
+
+### Animated
+
+Use controlled `value` to animate the progress indicator.
+
+
+
+### With Labels
+
+Compose with `Progress.Label` and `Progress.Value` for additional context.
+
+
+
+## Accessibility
+
+- Uses the `progressbar` ARIA role
+- Sets `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` attributes
+- Supports indeterminate state when `value` is `null`
+- Supports `aria-label` and `aria-valuetext` for screen readers
diff --git a/apps/www/src/content/docs/components/progress/props.ts b/apps/www/src/content/docs/components/progress/props.ts
new file mode 100644
index 00000000..a8523387
--- /dev/null
+++ b/apps/www/src/content/docs/components/progress/props.ts
@@ -0,0 +1,46 @@
+export interface ProgressProps {
+ /**
+ * The current value. Set to `null` for indeterminate state.
+ * @default 0
+ */
+ value?: number | null;
+
+ /**
+ * Minimum value.
+ * @default 0
+ */
+ min?: number;
+
+ /**
+ * Maximum value.
+ * @default 100
+ */
+ max?: number;
+
+ /**
+ * The visual style of the progress.
+ * @default "linear"
+ */
+ variant?: 'linear' | 'circular';
+
+ /** Additional CSS class name. */
+ className?: string;
+}
+
+export interface ProgressLabelProps {
+ /** The label text content. */
+ children: React.ReactNode;
+
+ /** Additional CSS class name. */
+ className?: string;
+}
+
+export interface ProgressValueProps {
+ /** Additional CSS class name. */
+ className?: string;
+}
+
+export interface ProgressTrackProps {
+ /** Additional CSS class name. */
+ className?: string;
+}
diff --git a/packages/raystack/components/progress/__tests__/progress.test.tsx b/packages/raystack/components/progress/__tests__/progress.test.tsx
new file mode 100644
index 00000000..780df8f6
--- /dev/null
+++ b/packages/raystack/components/progress/__tests__/progress.test.tsx
@@ -0,0 +1,127 @@
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { Progress } from '../progress';
+import styles from '../progress.module.css';
+
+describe('Progress', () => {
+ describe('Basic Rendering', () => {
+ it('renders progress element', () => {
+ const { container } = render();
+ const progress = container.querySelector(`.${styles.progress}`);
+ expect(progress).toBeInTheDocument();
+ });
+
+ it('forwards ref correctly', () => {
+ const ref = vi.fn();
+ render();
+ expect(ref).toHaveBeenCalled();
+ });
+
+ it('applies custom className', () => {
+ const { container } = render(
+
+ );
+ const progress = container.querySelector(`.${styles.progress}`);
+ expect(progress).toHaveClass('custom-progress');
+ });
+
+ it('renders track and indicator by default', () => {
+ const { container } = render();
+ expect(container.querySelector(`.${styles.track}`)).toBeInTheDocument();
+ expect(
+ container.querySelector(`.${styles.indicator}`)
+ ).toBeInTheDocument();
+ });
+
+ it('renders default track when no children provided', () => {
+ const { container } = render();
+ expect(container.querySelector(`.${styles.track}`)).toBeInTheDocument();
+ });
+ });
+
+ describe('Variants', () => {
+ it('defaults to linear variant', () => {
+ const { container } = render();
+ const progress = container.querySelector(`.${styles.progress}`);
+ expect(progress).not.toHaveClass(styles['progress-variant-circular']);
+ });
+
+ it('renders circular variant', () => {
+ const { container } = render();
+ const progress = container.querySelector(`.${styles.progress}`);
+ expect(progress).toHaveClass(styles['progress-variant-circular']);
+ });
+
+ it('renders SVG track and indicator for circular variant', () => {
+ const { container } = render();
+ expect(container.querySelector('svg')).toBeInTheDocument();
+ expect(
+ container.querySelector(`.${styles.circularTrackCircle}`)
+ ).toBeInTheDocument();
+ expect(
+ container.querySelector(`.${styles.circularIndicatorCircle}`)
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('Sub-components', () => {
+ it('renders Label sub-component', () => {
+ render(
+
+ );
+ expect(screen.getByText('Loading')).toBeInTheDocument();
+ });
+
+ it('renders Value sub-component', () => {
+ render(
+
+ );
+ expect(screen.getByText('50%')).toBeInTheDocument();
+ });
+
+ it('renders custom children instead of default track', () => {
+ const { container } = render(
+
+ );
+ expect(screen.getByText('Custom')).toBeInTheDocument();
+ expect(container.querySelector(`.${styles.track}`)).toBeInTheDocument();
+ });
+ });
+
+ describe('Indeterminate', () => {
+ it('supports null value for indeterminate state', () => {
+ render();
+ const progress = screen.getByRole('progressbar');
+ expect(progress).not.toHaveAttribute('aria-valuenow');
+ });
+ });
+
+ describe('Accessibility', () => {
+ it('has progressbar role', () => {
+ render();
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
+ });
+
+ it('sets aria-valuenow', () => {
+ render();
+ const progress = screen.getByRole('progressbar');
+ expect(progress).toHaveAttribute('aria-valuenow', '75');
+ });
+
+ it('sets aria-valuemin and aria-valuemax', () => {
+ render();
+ const progress = screen.getByRole('progressbar');
+ expect(progress).toHaveAttribute('aria-valuemin', '0');
+ expect(progress).toHaveAttribute('aria-valuemax', '200');
+ });
+ });
+});
diff --git a/packages/raystack/components/progress/index.tsx b/packages/raystack/components/progress/index.tsx
new file mode 100644
index 00000000..711a1140
--- /dev/null
+++ b/packages/raystack/components/progress/index.tsx
@@ -0,0 +1 @@
+export { Progress } from './progress';
diff --git a/packages/raystack/components/progress/progress-misc.tsx b/packages/raystack/components/progress/progress-misc.tsx
new file mode 100644
index 00000000..f1dd42f7
--- /dev/null
+++ b/packages/raystack/components/progress/progress-misc.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import { Progress as ProgressPrimitive } from '@base-ui/react';
+import { cx } from 'class-variance-authority';
+import { type ElementRef, forwardRef } from 'react';
+import styles from './progress.module.css';
+
+export const ProgressLabel = forwardRef<
+ ElementRef,
+ ProgressPrimitive.Label.Props
+>(({ className, ...props }, ref) => (
+
+));
+
+ProgressLabel.displayName = 'ProgressLabel';
+
+export const ProgressValue = forwardRef<
+ ElementRef,
+ ProgressPrimitive.Value.Props
+>(({ className, ...props }, ref) => (
+
+));
+
+ProgressValue.displayName = 'ProgressValue';
diff --git a/packages/raystack/components/progress/progress-root.tsx b/packages/raystack/components/progress/progress-root.tsx
new file mode 100644
index 00000000..f02adedd
--- /dev/null
+++ b/packages/raystack/components/progress/progress-root.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { Progress as ProgressPrimitive } from '@base-ui/react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { createContext, type ElementRef, forwardRef } from 'react';
+import styles from './progress.module.css';
+import { ProgressTrack } from './progress-track';
+
+const progress = cva(styles.progress, {
+ variants: {
+ variant: {
+ linear: '',
+ circular: styles['progress-variant-circular']
+ }
+ },
+ defaultVariants: {
+ variant: 'linear'
+ }
+});
+
+export interface ProgressProps
+ extends ProgressPrimitive.Root.Props,
+ VariantProps {}
+
+export interface ProgressContextValue {
+ variant: 'linear' | 'circular';
+ value: number | null;
+ percentage: number;
+}
+
+export const ProgressContext = createContext({
+ variant: 'linear',
+ value: 0,
+ percentage: 0
+});
+
+export const ProgressRoot = forwardRef<
+ ElementRef,
+ ProgressProps
+>(
+ (
+ {
+ className,
+ style = {},
+ variant = 'linear',
+ children = ,
+ value = 0,
+ min = 0,
+ max = 100,
+ ...props
+ },
+ ref
+ ) => {
+ const percentage = value === null ? 0 : ((value - min) * 100) / (max - min);
+
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+
+ProgressRoot.displayName = 'ProgressRoot';
diff --git a/packages/raystack/components/progress/progress-track.tsx b/packages/raystack/components/progress/progress-track.tsx
new file mode 100644
index 00000000..9c8e6ab8
--- /dev/null
+++ b/packages/raystack/components/progress/progress-track.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import { Progress as ProgressPrimitive } from '@base-ui/react';
+import { cx } from 'class-variance-authority';
+import { type ElementRef, forwardRef, useContext } from 'react';
+import styles from './progress.module.css';
+import { ProgressContext } from './progress-root';
+
+export const ProgressTrack = forwardRef<
+ ElementRef,
+ ProgressPrimitive.Track.Props
+>(({ className, children, ...props }, ref) => {
+ const { variant } = useContext(ProgressContext);
+
+ if (variant === 'circular') {
+ return (
+ (
+
+ )}
+ >
+ }
+ />
+ {children}
+
+ );
+ }
+
+ return (
+
+
+ {children}
+
+ );
+});
+
+ProgressTrack.displayName = 'ProgressTrack';
diff --git a/packages/raystack/components/progress/progress.module.css b/packages/raystack/components/progress/progress.module.css
new file mode 100644
index 00000000..324d5a0a
--- /dev/null
+++ b/packages/raystack/components/progress/progress.module.css
@@ -0,0 +1,97 @@
+.progress {
+ display: flex;
+ flex-direction: column;
+ gap: var(--rs-space-3);
+ width: 100%;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.label {
+ font-family: var(--rs-font-body);
+ font-size: var(--rs-font-size-mini);
+ font-weight: var(--rs-font-weight-medium);
+ line-height: var(--rs-line-height-mini);
+ letter-spacing: var(--rs-letter-spacing-mini);
+ color: var(--rs-color-foreground-base-primary);
+}
+
+.value {
+ font-family: var(--rs-font-body);
+ font-size: var(--rs-font-size-mini);
+ font-weight: var(--rs-font-weight-regular);
+ line-height: var(--rs-line-height-mini);
+ letter-spacing: var(--rs-letter-spacing-mini);
+ color: var(--rs-color-foreground-base-primary);
+ text-align: right;
+}
+
+.track {
+ position: relative;
+ width: 100%;
+ height: 4px;
+ overflow: clip;
+ border-radius: 1px;
+ background-color: var(--rs-color-background-neutral-secondary);
+}
+
+.indicator {
+ height: 100%;
+ background-color: var(--rs-color-background-accent-emphasis);
+ transition: width 500ms;
+}
+
+/* Circular variant — viewBox is 72×72, SVG scales to container size */
+.progress-variant-circular {
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.circularSvg {
+ --rs-progress-track-size: 4px;
+ --rs-progress-radius: calc((72px - var(--rs-progress-track-size) * 2) / 2);
+ --rs-progress-circumference: calc(2 * 3.14159265 * var(--rs-progress-radius));
+ width: 72px;
+ height: 72px;
+ aspect-ratio: 1;
+ transform: rotate(-90deg);
+}
+
+.circularTrackCircle,
+.circularIndicatorCircle {
+ cx: 50%;
+ cy: 50%;
+ r: var(--rs-progress-radius);
+ stroke-width: var(--rs-progress-track-size);
+ fill: none;
+}
+
+.circularTrackCircle {
+ stroke: var(--rs-color-background-neutral-secondary);
+}
+
+.circularIndicatorCircle {
+ stroke: var(--rs-color-background-accent-emphasis);
+ stroke-dasharray: var(--rs-progress-circumference);
+ stroke-dashoffset: calc(
+ var(--rs-progress-circumference) *
+ (1 - var(--rs-progress-percentage, 0) / 100)
+ );
+ stroke-linecap: butt;
+ transition: stroke-dashoffset 500ms;
+}
+
+.progress-variant-circular .value {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-weight: var(--rs-font-weight-medium);
+ text-align: center;
+ white-space: nowrap;
+}
diff --git a/packages/raystack/components/progress/progress.tsx b/packages/raystack/components/progress/progress.tsx
new file mode 100644
index 00000000..bdbf1cd8
--- /dev/null
+++ b/packages/raystack/components/progress/progress.tsx
@@ -0,0 +1,9 @@
+import { ProgressLabel, ProgressValue } from './progress-misc';
+import { ProgressRoot } from './progress-root';
+import { ProgressTrack } from './progress-track';
+
+export const Progress = Object.assign(ProgressRoot, {
+ Label: ProgressLabel,
+ Value: ProgressValue,
+ Track: ProgressTrack
+});
diff --git a/packages/raystack/index.tsx b/packages/raystack/index.tsx
index 95a9e531..70ee3ed7 100644
--- a/packages/raystack/index.tsx
+++ b/packages/raystack/index.tsx
@@ -46,6 +46,7 @@ export { Menu } from './components/menu';
export { Navbar } from './components/navbar';
export { Popover } from './components/popover';
export { PreviewCard } from './components/preview-card';
+export { Progress } from './components/progress';
export { Radio } from './components/radio';
export { ScrollArea } from './components/scroll-area';
export { Search } from './components/search';