HextaUI

Number flow

Yalnızca değişen basamakların döndüğü animasyonlu sayılar; herhangi bir Intl biçimi ve yerel ayarla.

22
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"

export function NumberFlowDemo() {
  const [value, setValue] = React.useState(22)

  return (
    <div className="flex flex-col items-center gap-6">
      <NumberFlow
        value={value}
        className="text-5xl font-semibold tracking-tight"
      />
      <div className="flex flex-wrap justify-center gap-2">
        <Button variant="outline" size="sm" onClick={() => setValue(value - 1)}>
          −1
        </Button>
        <Button variant="outline" size="sm" onClick={() => setValue(value + 1)}>
          +1
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(value + 111)}
        >
          +111
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(Math.floor(Math.random() * 100000))}
        >
          Random
        </Button>
      </div>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/number-flow.json

Bileşeni, HextaUI tema token'larını ve bileşenin bağımlı olduğu tüm HextaUI bileşenlerini ekler.

import { NumberFlow } from "@/components/ui/number-flow"
<NumberFlow
  value={1234.5}
  format={{ style: "currency", currency: "USD" }}
/>

Yalnızca değişen haneler hareket eder

Her hane kendi sütunudur. 22'den 23'e geçerken yalnızca birler hanesi döner; onlar hanesi sabit kalır. 99 → 100'deki üçüncü hane gibi beliren veya kaybolan haneler genişliklerini içeri veya dışarı kaydırır.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"

const pairs = [
  [22, 23],
  [14, 19],
  [99, 100],
  [19, 21],
]

export function NumberFlowChangedDigits() {
  const [value, setValue] = React.useState(22)

  return (
    <div className="flex flex-col items-center gap-6">
      <NumberFlow value={value} className="text-5xl font-semibold" />
      <div className="flex flex-wrap justify-center gap-2">
        {pairs.map(([from, to]) => (
          <Button
            key={`${from}-${to}`}
            variant="outline"
            size="sm"
            onClick={() => setValue(value === from ? to : from)}
          >
            {from} ↔ {to}
          </Button>
        ))}
      </div>
    </div>
  )
}

Eğilim

trend hanelerin hangi yöne döneceğini belirler. auto değer artarken yukarı, azalırken aşağı döner, up ve down bir yönü zorlar, shortest ise çarkın etrafında en yakın yolu seçer.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow, type NumberFlowTrend } from "@/components/ui/number-flow"

const trends: NumberFlowTrend[] = ["auto", "up", "down", "shortest"]

export function NumberFlowTrendDemo() {
  const [value, setValue] = React.useState(19)

  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
        {trends.map((trend) => (
          <div
            key={trend}
            className="flex flex-col gap-1 rounded-lg border p-3"
          >
            <span className="text-xs text-muted-foreground">{trend}</span>
            <NumberFlow
              value={value}
              trend={trend}
              className="text-2xl font-semibold"
            />
          </div>
        ))}
      </div>
      <div className="flex flex-wrap gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(value === 19 ? 21 : 19)}
        >
          19 ↔ 21
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(value === 19 ? 11 : 19)}
        >
          19 ↔ 11
        </Button>
      </div>
    </div>
  )
}

Biçimler

format ve locales her Intl.NumberFormat seçeneğini kabul eder. Haneler hareket ederken para birimi sembolleri, ayırıcılar ve işaretler sabit kalır; Latin olmayan rakamlar kendi yazılarında döner.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"

const factors = [1, 1.37, 0.62, 2.91]

export function NumberFlowFormats() {
  const [step, setStep] = React.useState(0)
  const factor = factors[step % factors.length]

  const rows = [
    {
      label: "Currency",
      value: (
        <NumberFlow
          value={1234.56 * factor}
          format={{ style: "currency", currency: "USD" }}
        />
      ),
    },
    {
      label: "Percent",
      value: (
        <NumberFlow
          value={0.4213 * factor}
          format={{ style: "percent", maximumFractionDigits: 1 }}
        />
      ),
    },
    {
      label: "Compact",
      value: (
        <NumberFlow
          value={1234567 * factor}
          format={{ notation: "compact", maximumFractionDigits: 1 }}
        />
      ),
    },
    {
      label: "Fixed decimals",
      value: (
        <NumberFlow
          value={3.14159 * factor}
          format={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }}
        />
      ),
    },
    {
      label: "de-DE",
      value: <NumberFlow value={9876543.21 * factor} locales="de-DE" />,
    },
    {
      label: "Signed",
      value: (
        <NumberFlow
          value={(factor - 1) * 100}
          format={{ signDisplay: "exceptZero", maximumFractionDigits: 0 }}
          suffix="%"
        />
      ),
    },
    {
      label: "ar-EG",
      value: (
        <NumberFlow
          value={4821 * factor}
          locales="ar-EG"
          format={{ maximumFractionDigits: 0 }}
        />
      ),
    },
    {
      label: "Prefix and suffix",
      value: (
        <NumberFlow
          value={Math.round(1840 * factor)}
          prefix="~"
          suffix=" users"
        />
      ),
    },
  ]

  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <dl className="grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-2">
        {rows.map((row) => (
          <div
            key={row.label}
            className="flex items-baseline justify-between gap-4 border-b py-1.5"
          >
            <dt className="text-muted-foreground">{row.label}</dt>
            <dd className="font-medium">{row.value}</dd>
          </div>
        ))}
      </dl>
      <Button
        variant="outline"
        size="sm"
        className="self-start"
        onClick={() => setStep(step + 1)}
      >
        Change values
      </Button>
    </div>
  )
}

Zamanlama

Hareketi duration ve easing ile ayarlayın veya animated={false} ile kapatın. Azaltılmış hareket ayarları onu otomatik olarak kapatır.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"

export function NumberFlowTiming() {
  const [value, setValue] = React.useState(128)

  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
        <div className="flex flex-col gap-1 rounded-lg border p-3">
          <span className="text-xs text-muted-foreground">200ms</span>
          <NumberFlow
            value={value}
            duration={200}
            className="text-2xl font-semibold"
          />
        </div>
        <div className="flex flex-col gap-1 rounded-lg border p-3">
          <span className="text-xs text-muted-foreground">600ms</span>
          <NumberFlow value={value} className="text-2xl font-semibold" />
        </div>
        <div className="flex flex-col gap-1 rounded-lg border p-3">
          <span className="text-xs text-muted-foreground">1200ms</span>
          <NumberFlow
            value={value}
            duration={1200}
            easing="ease-in-out"
            className="text-2xl font-semibold"
          />
        </div>
        <div className="flex flex-col gap-1 rounded-lg border p-3">
          <span className="text-xs text-muted-foreground">Off</span>
          <NumberFlow
            value={value}
            animated={false}
            className="text-2xl font-semibold"
          />
        </div>
      </div>
      <Button
        variant="outline"
        size="sm"
        className="self-start"
        onClick={() => setValue(value + 37)}
      >
        +37
      </Button>
    </div>
  )
}

Kesintiler ve olaylar

Dönüş sırasında gelen yeni bir değer her hanenin bulunduğu yerden devam eder, böylece hızlı güncellemeler hiç zıplamaz. onAnimationsStart ve onAnimationsFinish her hane grubunda bir kez tetiklenir.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"

export function NumberFlowEvents() {
  const [value, setValue] = React.useState(1200)
  const [status, setStatus] = React.useState("Idle")
  const timers = React.useRef<number[]>([])

  React.useEffect(() => {
    const pending = timers.current
    return () => pending.forEach((id) => window.clearTimeout(id))
  }, [])

  function burst() {
    timers.current.forEach((id) => window.clearTimeout(id))
    timers.current = Array.from({ length: 10 }, (_, index) =>
      window.setTimeout(
        () => setValue((current) => current + Math.ceil(Math.random() * 40)),
        index * 90
      )
    )
  }

  return (
    <div className="flex flex-col items-center gap-4">
      <NumberFlow
        value={value}
        onAnimationsStart={() => setStatus("Animating")}
        onAnimationsFinish={() => setStatus("Settled")}
        className="text-5xl font-semibold"
      />
      <span className="text-sm text-muted-foreground">{status}</span>
      <Button variant="outline" size="sm" onClick={burst}>
        Burst ×10
      </Button>
    </div>
  )
}

Satır içi ve rozetlerde

Metnin taban çizgisine oturan satır içi bir <span> işler; haneler dönerken tabular haneler genişliği sabit tutar. BadgeCount bunu en fazla bir üst sınırda duran sayımlar için kullanır.

"use client"

import * as React from "react"
import { IconInbox } from "@tabler/icons-react"

import { Badge, BadgeCount } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"

export function NumberFlowInline() {
  const [value, setValue] = React.useState(8)

  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <p className="text-sm">
        You have <NumberFlow value={value} className="font-semibold" /> new
        messages and the baseline stays aligned with the text.
      </p>
      <div className="flex flex-wrap items-center gap-2">
        <Button variant="outline">
          <IconInbox />
          Inbox
          <Badge size="sm" appearance="solid" variant="destructive">
            <BadgeCount value={value} />
          </Badge>
        </Button>
        <Badge>
          <BadgeCount value={value * 7} max={999} />
          unread
        </Badge>
        <Button variant="outline" size="sm" onClick={() => setValue(value + 1)}>
          +1
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(value + 50)}
        >
          +50
        </Button>
      </div>
    </div>
  )
}

Sağdan sola

Sayılar, tarayıcıların onları işleyişine uygun olarak, sağdan sola düzende bile her zaman soldan sağa okunur.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"

export function NumberFlowRtl() {
  const [value, setValue] = React.useState(1250)

  return (
    <div dir="rtl" className="flex flex-col items-center gap-4">
      <NumberFlow
        value={value}
        locales="ar-EG"
        className="text-3xl font-semibold"
      />
      <Button variant="outline" size="sm" onClick={() => setValue(value + 17)}>
        ١٧+
      </Button>
    </div>
  )
}
  • Ekran okuyucular gerçek biçimlendirilmiş sayıyı okur. Dönen sütunlar yardımcı teknolojilerden gizlenir; ayrılan karakterler animasyonla çıkarken gizlenir.
  • Canlı bölge değildir. Değişikliklerin duyurulması gerekiyorsa onu aria-live="polite" olan bir öğeyle sarın.
  • Azaltılmış hareket açıkken değerler dönmeden anında güncellenir.

children ve prefix dışındaki tüm <span> niteliklerini kabul eder.

PropTürVarsayılan
valueZorunlu.
number–
locales
Intl.LocalesArgument"en-US"
formatIntl.NumberFormat'a iletilir.
Intl.NumberFormatOptions–
prefixSayıdan önceki sabit metin.
string–
suffixSayıdan sonraki sabit metin.
string–
trend
"auto" | "up" | "down" | "shortest""auto"
durationDönüş süresi, milisaniye cinsinden.
number600
easingHerhangi bir CSS easing değeri.
stringeaseSpring
animated
booleantrue
onAnimationsStartBir hane grubu hareket etmeye başladığında çağrılır.
() => void–
onAnimationsFinishSon grup oturduğunda çağrılır. Daha yeni bir değer onu keserse atlanır.
() => void–
ÖznitelikAçıklama
data-slot="number-flow"Kök span.
data-slot="number-flow-digit"Her hane hücresi.
data-slot="number-flow-symbol"Ayırıcılar, işaretler, semboller, önek ve sonek.
data-slot="number-flow-column"Hane hücresinin içindeki dönen sütun.
data-digitBir sütunun üzerinde durduğu hane, 0–9.

Kullanıldığı bloklar

Number flow üzerine kurulan bloklar.