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 }