Tracker
A component that visualizes progress through a series of steps or stages in a process.
Automatic Installation
This method is still working on progress, please use manual installation for now.
Manual Installation
Install dependencies
npm install radix-ui
Copy the code
import { HoverCard as HoverCardPrimitive } from 'radix-ui'
import * as React from 'react'
import { type TrackerStyles, trackerStyles } from './tracker.css'
interface TrackerBlockProps extends TrackerStyles {
key?: string | number
color?: string
tooltip?: string | React.ReactNode
defaultColor?: string
onClick?: () => void
ariaLabel?: string
disabled?: boolean
}
const Block = ({
color,
tooltip,
defaultColor,
hoverEffect,
flatten,
hideArrow,
onClick,
ariaLabel,
disabled = false,
}: TrackerBlockProps) => {
const [open, setOpen] = React.useState(false)
const styles = trackerStyles({ hoverEffect, flatten, hideArrow })
if (!tooltip) {
return (
<div className={styles.block()}>
<div
className={styles.blockInner({ className: color || defaultColor })}
role="presentation"
/>
</div>
)
}
// Make sure ariaLabel is always available
const accessibleLabel = ariaLabel || (typeof tooltip === 'string' ? tooltip : 'Tracker item')
const handleOnClick = (e: React.MouseEvent) => {
if (onClick) {
e.preventDefault()
onClick()
}
setOpen(true)
}
return (
<HoverCardPrimitive.Root open={open} onOpenChange={setOpen} openDelay={30} closeDelay={60}>
<HoverCardPrimitive.Trigger asChild>
<button
type="button"
className={styles.block()}
onClick={handleOnClick}
aria-label={accessibleLabel}
disabled={disabled}
>
<span className="sr-only">{accessibleLabel}</span>
<div
className={styles.blockInner({ className: color || defaultColor })}
aria-hidden="true"
/>
</button>
</HoverCardPrimitive.Trigger>
<HoverCardPrimitive.Portal>
<HoverCardPrimitive.Content
align="center"
side="top"
sideOffset={10}
className={styles.tooltip()}
avoidCollisions
>
{tooltip}
{!hideArrow && (
<HoverCardPrimitive.Arrow
className={styles.arrow()}
aria-hidden="true"
width={12}
height={7}
/>
)}
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Portal>
</HoverCardPrimitive.Root>
)
}
interface TrackerProps extends React.HTMLAttributes<HTMLDivElement>, TrackerStyles {
data: TrackerBlockProps[]
defaultColor?: string
emptyColor?: string
}
const Tracker = React.forwardRef<HTMLDivElement, TrackerProps>(
(
{
data = [],
defaultColor = 'bg-muted',
className,
hoverEffect,
flatten,
size,
hideArrow = false,
emptyColor = 'bg-muted/30',
...props
},
forwardedRef
) => {
const styles = trackerStyles({ hoverEffect, size, hideArrow, flatten })
// Handle empty data case
if (data.length === 0) {
return (
<div ref={forwardedRef} className={styles.base({ className })} {...props}>
<div className={styles.block()}>
<div className={styles.blockInner({ className: emptyColor })} />
</div>
</div>
)
}
return (
<div ref={forwardedRef} className={styles.base({ className })} {...props}>
{data.map((blockProps, index) => (
<Block
key={blockProps.key ?? index}
defaultColor={defaultColor}
hoverEffect={hoverEffect}
flatten={flatten}
hideArrow={hideArrow}
{...blockProps}
/>
))}
</div>
)
}
)
Block.displayName = 'TrackerBlock'
Tracker.displayName = 'Tracker'
export { Tracker, type TrackerBlockProps, type TrackerProps }
Usage
Imports
import { Tracker } from '#/components/tracker'
Example
Browse the Storybook for more examples.
Props
Prop | Type | Default |
---|---|---|
data? | TrackerBlockProps[] | [] |
size? | sm | md | lg | md |
hoverEffect? | boolean | - |
hideArrow? | boolean | false |
defaultBackgroundColor? | string | bg-muted |
emptyColor? | string | bg-muted/30 |
className? | string | - |
Edit on GitHub
Last updated on 3/27/2025