Components
Progress Button
A button component that shows progress and can be used for actions like form submission.
Component Source'use client'import * as React from 'react'import { ProgressButton } from '@/components/ui/progress-button'export default function ProgressButtonDemo() { const [progress, setProgress] = React.useState(0) React.useEffect(() => { const interval = setInterval(() => { setProgress((prev) => { if (prev >= 100) { clearInterval(interval) return 100 } return prev + 10 }) }, 200) return () => { clearInterval(interval) } }, [progress]) return ( <div className='flex gap-4'> {( ['default', 'secondary', 'destructive', 'outline', 'ghost'] as const ).map((variant) => ( <ProgressButton key={variant} variant={variant} progress={progress} isLoading={progress < 100} onClick={() => { setProgress(0) }} > Submit </ProgressButton> ))} </div> )}
Installation
CLI
npx shadcn add https://ui.tiesen.id.vn/r/progress-button.json
npx shadcn add https://ui.tiesen.id.vn/r/progress-button.json
pnpm dlx shadcn add https://ui.tiesen.id.vn/r/progress-button.json
bunx --bun shadcn add https://ui.tiesen.id.vn/r/progress-button.json
Manual
Copy and paste the following code into your project.
import * as React from 'react'import { cn } from '@/lib/utils'import { Button } from '@/components/ui/button'interface ProgressButtonProps extends React.ComponentProps<typeof Button> { progress: number minProgress?: number maxProgress?: number isLoading?: boolean}function ProgressButton({ progress, minProgress = 0, maxProgress = 100, isLoading = false, className, children, onMouseEnter, onMouseLeave, ...props}: ProgressButtonProps) { const { normalizedProgress, progressPercentage } = React.useMemo(() => { const normalizedProgress = Math.min( Math.max(progress, minProgress), maxProgress, ) const progressPercentage = ((normalizedProgress - minProgress) / (maxProgress - minProgress)) * 100 return { normalizedProgress, progressPercentage } }, [progress, minProgress, maxProgress]) const style = React.useMemo(() => { if (props.variant === 'destructive') return { '--default': 'var(--destructive)', '--active': 'var(--default)', '--hover': 'color-mix(in oklab, var(--destructive) 90%, transparent)', background: `linear-gradient(to right, var(--active) ${progressPercentage}%, var(--secondary) ${progressPercentage}%)`, } else if (props.variant === 'secondary') return { '--default': 'var(--secondary)', '--hover': 'color-mix(in oklab, var(--secondary) 90%, transparent)', '--active': 'var(--default)', background: `linear-gradient(to right, var(--active) ${progressPercentage}%, var(--primary) ${progressPercentage}%)`, } else if (props.variant === 'outline') return { '--default': 'var(--background)', '--hover': 'var(--accent)', '--active': 'var(--default)', background: `linear-gradient(to right, var(--active) ${progressPercentage}%, var(--accent) ${progressPercentage}%)`, } else if (props.variant === 'ghost') return { '--default': 'transparent', '--hover': 'var(--accent)', '--active': ' var(--default)', background: `linear-gradient(to right, var(--active) ${progressPercentage}%, var(--accent) ${progressPercentage}%)`, } return { '--default': 'var(--primary)', '--hover': 'color-mix(in oklab, var(--primary) 90%, transparent)', '--active': 'var(--default)', background: `linear-gradient(to right, var(--active) ${progressPercentage}%, var(--secondary) ${progressPercentage}%)`, } }, [progressPercentage, props.variant]) const handleMouseEnter = React.useCallback( (e: React.MouseEvent<HTMLButtonElement>) => { e.currentTarget.style.setProperty('--active', 'var(--hover)') onMouseEnter?.(e) }, [onMouseEnter], ) const handleMouseLeave = React.useCallback( (e: React.MouseEvent<HTMLButtonElement>) => { e.currentTarget.style.setProperty('--active', 'var(--default)') onMouseLeave?.(e) }, [onMouseLeave], ) return ( <Button {...props} data-slot='loading-button' role='progressbar' disabled={isLoading} style={style} className={cn( 'group/loading-button relative transition-colors', 'disabled:text-transparent disabled:opacity-100', className, )} aria-busy={isLoading} aria-valuemin={minProgress} aria-valuemax={maxProgress} aria-valuenow={normalizedProgress} aria-label={ isLoading ? `Progress: ${Math.round(progressPercentage)}%` : undefined } onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > {children} <span className='absolute inset-0 hidden items-center justify-center text-sm font-medium text-background mix-blend-difference group-disabled/loading-button:flex dark:text-foreground'> {Math.round(progressPercentage)}% </span> </Button> )}export { ProgressButton }