Page dots
FreeWhere you are in a set of slides: the current dot stretches into a pill that fills while slides advance on their own.
pnpm dlx shadcn@latest add https://caliperui.site/r/page-dots.jsonMatch payments in any currency
When to use #
Use it for
- Carousels, onboarding steps and image sets.
- Any pager with a handful of pages.
Skip it when
- Long sets; show a count and arrows.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/page-dots.json
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
example.tsx
import { useEffect, useRef, useState } from "react" import { PageDots } from "@/components/ui/page-dots" const motion = ["slide-to-confirm", "progress-button", "secret-field", "character-ring", "range-histogram", "transfer-list", "zoom-lens", "page-dots", "swipe-tabs", "drill-down-menu"]; export function PageDotsDemo() { const slides = ["Match payments in any currency", "Bank feeds every 15 minutes", "Rules on IBAN and reference", "Exports that keep your columns"]; const [index, setIndex] = useState(0); const [progress, setProgress] = useState(0); const [paused, setPaused] = useState(false); const elapsed = useRef(0); useEffect(() => { if (paused || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; const id = setInterval(() => { elapsed.current += 0.025; if (elapsed.current >= 1) { elapsed.current = 0; setIndex((current) => (current + 1) % slides.length); } setProgress(elapsed.current); }, 100); return () => clearInterval(id); }, [paused, slides.length]); return ( <div className="grid w-full max-w-sm justify-items-center gap-4" onPointerEnter={() => setPaused(true)} onPointerLeave={() => setPaused(false)} onFocus={() => setPaused(true)} onBlur={() => setPaused(false)}> <div className="grid h-28 w-full place-items-center rounded-[14px] bg-muted px-6 text-center text-sm font-medium" aria-live="polite">{slides[index]}</div> <PageDots count={slides.length} index={index} onIndexChange={(next) => { elapsed.current = 0; setIndex(next); setProgress(0); }} label="What's new" progress={paused ? undefined : progress} /> </div> ); }
API reference #
PageDots
component
PropTypeDefaultDescription
count *number—
index *number—
onIndexChange *(index: number) => void—
labelstring"Slides"Names the group, e.g. "Testimonials".
progressnumber—0 to 1: how far the current slide's timer has run, for slides that advance by themselves.
classNamestring—
Accessibility #
It exposes group semantics, with aria-label, aria-current and aria-hidden kept in step with state.
KeyBehaviour
→Moves to the next item or value.
←Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.
Motion #
Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.