caliper uidocs

Page dots

Free

Where you are in a set of slides: the current dot stretches into a pill that fills while slides advance on their own.

Installs components/ui/page-dots.tsxDepends on none beyond ReactMotion shared motion tokens
pnpm dlx shadcn@latest add https://caliperui.site/r/page-dots.json
Match 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.