nextui.dev
Home
Library
Docs
Pricing
Components26
AI & agents5
  • AI chat inputPro
  • Agent task checklistPro
  • Agent thinking
  • Thinking indicator
  • Prompt deckNew
Forms & inputs7
  • Input with label
  • Input with icon and button
  • Settings switches
  • Password with strength meter
  • One-time code input
  • WaitlistNew
  • File dropzonePro
Cards5
  • Style marquee cardPro
  • Folder card
  • Celebration cardPro
  • Smart PDF stackNew
  • Waitlist pass
Buttons, badges & avatars5
  • Buttons with icons
  • QR scan badge
  • Avatar group
  • Rolling price badge
  • Avatars with status
Feedback & display4
  • Alert banners
  • FAQ accordion
  • Keyboard shortcuts
  • Empty state
Blocks11
Hero4
  • Animated pill heroPro
  • Integrations grid hero
  • Waitlist heroNew
  • Orbit chat heroNew
Features & pricing3
  • Usage cardPro
  • Three-tier pricing
  • Feature with board
Testimonials & team3
  • Testimonial cards
  • Single quote
  • Team carousel
Sign-in1
  • Sign-in cardPro
Motion2
Effects2
  • Text revealPro
  • Sound ringNew
Themes9
  • Default
  • Ember
  • Ocean
  • Forest
  • Grape
  • Rose
  • Amber
  • Clay
  • Brutal
nextui.dev

nextui components, blocks and themes you install with one command.

Product

  • Components
  • Blocks
  • Motion
  • Themes
  • Pricing

Resources

  • Docs
  • Roadmap
  • GitHub

Legal

  • License
  • Privacy
  • Terms
  • Refunds

nextUI

[ 200 OK ] · nextUI

© 2026 NEXTUI.DEV. All rights reserved.

ComponentsButtons, badges & avatarsRolling price badge

Rolling price badge

Pill badge that rolls between prices glyph by glyph, resizing smoothly as it goes.

Copy prompt
MCP

Installation

Runs as is, no setup needed. Want a shorter command like @nextui/badge-02? See short names.

$pnpm dlx shadcn@latest add https://tinistudio.vercel.app/r/badge-02.json

Related

All components

Buttons with icons

Secondary, outline and destructive buttons with soft status buttons and animated icons, popping in as one group.

QR scan badge

A QR code that dissolves into sand, pours into a scan badge and grows back.

Avatar group

Stacked avatars with an overflow count.

npx shadcn@latest add https://tinistudio.vercel.app/r/badge-02.json
components/nextui/badge-02.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { AnimatePresence, MotionConfig, motion, useInView, useReducedMotion } from "motion/react";
import { Badge } from "@/components/ui/badge";

// The badge steps through these, one every HOLD ms, while it is on screen.
const VALUES = ["$17.38", "$3.15", "$35.99", "Free"];
const HOLD = 1800;
const STAGGER = 0.035; // seconds between neighbouring glyphs, so a change ripples across

// Low damping lets each glyph overshoot as it rises, so the text dips and settles.
const spring = { type: "spring", stiffness: 520, damping: 24, mass: 0.7 } as const;

type Glyph = { key: string; char: string };

// Prices are keyed around the decimal point, so cents swap with cents and units with units,
// and a dropped digit leaves from the front. Words are keyed left to right.
function glyphs(value: string): Glyph[] {
  const m = /^(\D*)(\d+)(?:\.(\d+))?$/.exec(value);
  if (!m) return [...value].map((char, i) => ({ key: `t${i}`, char }));
  const [, prefix, int, frac] = m;
  return [
    ...[...prefix].map((char, i) => ({ key: `p${i}`, char })),
    ...[...int].map((char, i) => ({ key: `i${int.length - 1 - i}`, char })),
    ...(frac ? [{ key: "dot", char: "." }, ...[...frac].map((char, i) => ({ key: `f${i}`, char }))] : []),
  ];
}

export default function Badge02() {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref);
  const reduce = useReducedMotion();
  const [index, setIndex] = useState(0);
  const value = VALUES[index];

  // Loops only run while the badge is on screen, and never under reduced motion.
  useEffect(() => {
    if (!inView || reduce) return;
    const id = window.setInterval(() => {
      setIndex((i) => (i + 1) % VALUES.length);
    }, HOLD);
    return () => window.clearInterval(id);
  }, [inView, reduce]);

  const list = glyphs(value);

  return (
    <MotionConfig transition={spring} reducedMotion="user">
      <div ref={ref} className="flex items-center justify-center p-4">
        <Badge asChild className="h-7 gap-0 rounded-full px-3 text-sm font-normal tabular-nums">
          {/* The pill only resizes, without overshoot; the bounce belongs to the text. */}
          <motion.span layout style={{ borderRadius: 999 }} transition={{ layout: { type: "spring", stiffness: 420, damping: 40 } }}>
            <span className="sr-only">{value}</span>
            <span aria-hidden className="flex">
              <AnimatePresence mode="popLayout" initial={false}>
                {list.map(({ key, char }, i) => (
                  <motion.span
                    key={key}
                    layout="position"
                    className="relative inline-flex"
                    initial={{ opacity: 0, scale: 0.6 }}
                    animate={{ opacity: 1, scale: 1 }}
                    exit={{ opacity: 0, scale: 0.6 }}
                  >
                    <AnimatePresence mode="popLayout" initial={false}>
                      <motion.span
                        key={char}
                        className="inline-block"
                        variants={{
                          enter: { y: "0.55em", scale: 0.5, opacity: 0 },
                          center: { y: 0, scale: 1, opacity: 1 },
                          exit: { y: "0.55em", scale: 0.5, opacity: 0 },
                        }}
                        initial="enter"
                        animate="center"
                        exit="exit"
                        transition={{ ...spring, delay: i * STAGGER }}
                      >
                        {char}
                      </motion.span>
                    </AnimatePresence>
                  </motion.span>
                ))}
              </AnimatePresence>
            </span>
          </motion.span>
        </Badge>
      </div>
    </MotionConfig>
  );
}
Sign in