Skip to content
edmi-ui

Progress

Shows how far a task is. Label and value parts; brand variant.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
x
Raising for launch
x
Raising for launch20%
Raising for launch 20%
Terminal window
npx shadcn@latest add @edmi-ui/progress
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/progress
Terminal window
yarn dlx shadcn@latest add @edmi-ui/progress
Terminal window
bunx shadcn@latest add @edmi-ui/progress

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/progress
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/progress
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/progress
Terminal window
bunx shadcn-vue@latest add @edmi-ui/progress

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/progress.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/progress.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/progress.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/progress.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress";
<div className="flex w-80 flex-col gap-6">
<Progress />
<Progress variant="brand">
<ProgressLabel>Raising for launch</ProgressLabel>
<ProgressValue />
</Progress>
</div>
<script setup lang="ts">
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress";
</script>
<template>
<div class="flex w-80 flex-col gap-6">
<Progress />
<Progress variant="brand">
<ProgressLabel>Raising for launch</ProgressLabel>
<ProgressValue />
</Progress>
</div>
</template>
<script lang="ts">
import * as Progress from "$lib/components/ui/progress";
</script>
<div class="flex w-80 flex-col gap-6">
<Progress.Root />
<Progress.Root variant="brand">
<Progress.Label>Raising for launch</Progress.Label>
<Progress.Value />
</Progress.Root>
</div>
registry/ui/progress.tsx
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
import { cn } from "cn";
function Progress({
className,
children,
value,
variant = "default",
...props
}: ProgressPrimitive.Root.Props & { variant?: "default" | "brand" }) {
return (
<ProgressPrimitive.Root
value={value}
data-slot="progress"
data-variant={variant}
className={cn("flex flex-wrap gap-3", className)}
{...props}
>
{children}
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
</ProgressPrimitive.Root>
);
}
// 8px track: muted fill + 1px border, indicator is --primary (✦ variant="brand" on Progress).
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
return (
<ProgressPrimitive.Track
className={cn(
"relative flex h-2 w-full items-center overflow-hidden rounded-full border border-border bg-muted",
className,
)}
data-slot="progress-track"
{...props}
/>
);
}
function ProgressIndicator({
className,
...props
}: ProgressPrimitive.Indicator.Props) {
return (
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className={cn(
"h-full rounded-full bg-primary transition-all [[data-variant=brand]_&]:bg-brand",
className,
)}
{...props}
/>
);
}
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
return (
<ProgressPrimitive.Label
className={cn("text-sm font-medium", className)}
data-slot="progress-label"
{...props}
/>
);
}
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
return (
<ProgressPrimitive.Value
className={cn(
"ml-auto font-mono text-[13px] text-muted-foreground tabular-nums",
className,
)}
data-slot="progress-value"
{...props}
/>
);
}
export {
Progress,
ProgressIndicator,
ProgressLabel,
ProgressTrack,
ProgressValue,
};
registry/ui/progress/Progress.vue
<script setup lang="ts">
import type { ProgressRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ProgressIndicator, ProgressRoot } from 'reka-ui'
import { computed, provide } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { progressPercentKey } from './context'
const props = withDefaults(
defineProps<ProgressRootProps & {
class?: HTMLAttributes['class']
variant?: 'default' | 'brand'
}>(),
{
modelValue: 0,
variant: 'default',
},
)
const delegatedProps = reactiveOmit(props, 'class', 'variant')
const percent = computed(() =>
props.modelValue == null ? null : Math.round((props.modelValue / (props.max ?? 100)) * 100),
)
provide(progressPercentKey, percent)
</script>
<template>
<ProgressRoot
data-slot="progress"
:data-variant="variant"
v-bind="delegatedProps"
:class="cn('flex flex-wrap gap-3', props.class)"
>
<slot />
<!-- 8px track: muted fill + 1px border, indicator is --primary (✦ variant="brand"). -->
<div
data-slot="progress-track"
class="relative flex h-2 w-full items-center overflow-hidden rounded-full border border-border bg-muted"
>
<ProgressIndicator
data-slot="progress-indicator"
class="h-full w-full flex-1 rounded-full bg-primary transition-all [[data-variant=brand]_&]:bg-brand"
:style="`transform: translateX(-${100 - (percent ?? 0)}%);`"
/>
</div>
</ProgressRoot>
</template>
registry/ui/progress/ProgressLabel.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span
data-slot="progress-label"
:class="cn('text-sm font-medium', props.class)"
>
<slot />
</span>
</template>
registry/ui/progress/ProgressValue.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { inject } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { progressPercentKey } from './context'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const percent = inject(progressPercentKey, undefined)
</script>
<template>
<span
data-slot="progress-value"
:class="cn('ml-auto font-mono text-[13px] text-muted-foreground tabular-nums', props.class)"
>
<slot>{{ percent == null ? "" : `${percent}%` }}</slot>
</span>
</template>
registry/ui/progress/context.ts
import type { ComputedRef, InjectionKey } from "vue";
export const progressPercentKey: InjectionKey<ComputedRef<number | null>> =
Symbol("edmi-progress-percent");
registry/ui/progress/index.ts
export { default as Progress } from "./Progress.vue";
export { default as ProgressLabel } from "./ProgressLabel.vue";
export { default as ProgressValue } from "./ProgressValue.vue";
src/lib/registry/ui/progress/context.ts
import { getContext, setContext } from "svelte";
const key = Symbol("edmi-progress-percent");
/** Getter for the current percent (0-100), or null when indeterminate. */
export function setProgressPercent(get: () => number | null) {
setContext(key, get);
}
export function getProgressPercent(): () => number | null {
return getContext<(() => number | null) | undefined>(key) ?? (() => null);
}
src/lib/registry/ui/progress/progress-label.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="progress-label"
class={cn(
"text-sm font-medium",
className
)}
{...restProps}
>
{@render children?.()}
</span>
src/lib/registry/ui/progress/progress-value.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { getProgressPercent } from "./context.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
const percent = getProgressPercent();
</script>
<span
bind:this={ref}
data-slot="progress-value"
class={cn(
"ml-auto font-mono text-[13px] text-muted-foreground tabular-nums",
className
)}
{...restProps}
>
{#if children}{@render children()}{:else if percent() != null}{percent()}%{/if}
</span>
src/lib/registry/ui/progress/progress.svelte
<script lang="ts">
import { Progress as ProgressPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { setProgressPercent } from "./context.js";
let {
ref = $bindable(null),
class: className,
max = 100,
value,
variant = "default",
children,
...restProps
}: Omit<ProgressPrimitive.RootProps, "child"> & { variant?: "default" | "brand" } = $props();
const percent = $derived(value == null ? null : Math.round((100 * value) / (max ?? 1)));
setProgressPercent(() => percent);
</script>
<ProgressPrimitive.Root
bind:ref
data-slot="progress"
data-variant={variant}
class={cn("flex flex-wrap gap-3", className)}
{value}
{max}
{...restProps}
>
{@render children?.()}
<!-- 8px track: muted fill + 1px border, indicator is --primary (✦ variant="brand"). -->
<div data-slot="progress-track" class="relative flex h-2 w-full items-center overflow-hidden rounded-full border border-border bg-muted">
<div
data-slot="progress-indicator"
class="h-full w-full flex-1 rounded-full bg-primary transition-all [[data-variant=brand]_&]:bg-brand"
style="transform: translateX(-{100 - (percent ?? 0)}%)"
></div>
</div>
</ProgressPrimitive.Root>
src/lib/registry/ui/progress/index.ts
import Root from "./progress.svelte";
import Label from "./progress-label.svelte";
import Value from "./progress-value.svelte";
export {
Label,
Label as ProgressLabel,
Root,
//
Root as Progress,
Value,
Value as ProgressValue,
};
Prop Type Default
value number | null null
variant default | brand default

Parts: Progress, ProgressLabel, ProgressValue, ProgressTrack, ProgressIndicator.

  • variant="brand" fills with --brand; track is 8px with a 1px border; value is mono.