HextaUI

Hareket

Her bileşenin kullandığı yumuşatma eğrileri, süreler ve azaltılmış hareket denetimi; ayrıca boyut dönüşümleri ve kayan vurgular için hook'lar.

easeOut
easeInOut
easeSpring
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  duration,
  easeInOut,
  easeOut,
  easeSpring,
  prefersReducedMotion,
} from "@/lib/motion"

const curves = [
  { name: "easeOut", easing: easeOut },
  { name: "easeInOut", easing: easeInOut },
  { name: "easeSpring", easing: easeSpring },
]

export function MotionEasing() {
  const dots = React.useRef<(HTMLSpanElement | null)[]>([])
  const [forward, setForward] = React.useState(true)

  const play = () => {
    dots.current.forEach((dot) => {
      if (!dot) {
        return
      }
      const track = dot.parentElement?.clientWidth ?? 0
      const distance = track - dot.offsetWidth
      dot.animate(
        [
          { translate: `${forward ? 0 : distance}px 0` },
          { translate: `${forward ? distance : 0}px 0` },
        ],
        {
          duration: prefersReducedMotion() ? 0 : duration.morph * 2,
          easing: curves[dots.current.indexOf(dot)].easing,
          fill: "forwards",
        }
      )
    })
    setForward((value) => !value)
  }

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      {curves.map((curve, index) => (
        <div key={curve.name} className="flex flex-col gap-1.5">
          <span className="font-mono text-xs text-muted-foreground">
            {curve.name}
          </span>
          <div dir="ltr" className="h-3 rounded-full bg-muted">
            <span
              ref={(node) => {
                dots.current[index] = node
              }}
              className="block size-3 rounded-full bg-foreground"
            />
          </div>
        </div>
      ))}
      <Button variant="outline" size="sm" onClick={play}>
        Play at {duration.morph * 2}ms
      </Button>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/motion.json

Yardımcıyı (utility) ve bağımlı olduğu her şeyi projenize ekler.

Her HextaUI bileşeni aynı birkaç eğri ve süreyle hareket eder, böylece kütüphane tek bir bütün gibi hissedilir.

  • Size yanıt veren şeyler için ease out. Giren, genişleyen veya tıklamayı izleyen öğeler hızlı başlar ve yavaşlayarak oturur; arayüz anlık hissedilir.
  • Kısa ve kesilebilir. Hareketin çoğu 150 ila 300ms sürer. Geri çevrilebilen her şey yeniden başlamak yerine şu anki yerinden başlar.
  • Azaltılmış hareket bir kapatma anahtarı değil, ikinci bir tasarımdır. Hareket anlık değişikliklere veya düz solmalara dönüşür ve durum okunabilir kalır.

Tema eğrileri Tailwind easing yardımcıları olarak tanımlar; lib/motion aynı değerleri Web Animations API için dışa aktarır.

SınıfAçıklama
ease-out-quintJS'te easeOut. Hareket için varsayılan: popover'lar, vurgular, boyut değişiklikleri.
ease-out-cubicHover ve odakta renk ve gölge değişimleri için daha yumuşak bir ease out.
ease-in-out-quartJS'te easeInOut. Kimsenin doğrudan tetiklemediği iki durgun durum arasındaki hareket için.
ease-springJS'te easeSpring. Küçük bir aşımı olan, linear() olarak yazılmış bir yay; anahtarın düğmesi gibi yerine oturan şeyler için.
ease-drawerBir kenardan kayarak gelen drawer ve sheet'ler için iOS sheet eğrisi.
<div className="transition-transform duration-300 ease-out-quint motion-reduce:transition-none" />
<div className="transition-colors duration-150 ease-out-cubic" />
<aside className="transition-transform duration-500 ease-drawer" />
import { duration, easeOut, prefersReducedMotion } from "@/lib/motion"

element.animate(
  [{ opacity: 0, translate: "0 4px" }, { opacity: 1, translate: "0 0" }],
  {
    duration: prefersReducedMotion() ? 0 : duration.enter,
    easing: easeOut,
  }
)
duration.Açıklama
press: 100Aşağı inen basılı durum.
release: 200Basıştan sonra geri yükselme.
hover: 150Hover ve odak geri bildirimi.
enter: 200Beliren öğeler.
exit: 150Ayrılan öğeler. Çıkışlar girişlerden hızlıdır, böylece hiçbir şeyi bekletmezler.
morph: 300Boyut ve konum değişiklikleri.

prefersReducedMotion() medya sorgusunu çağrı anında okur. Sistem ayarı değiştiğinde hemen uygulanması için bir kez mount sırasında değil, bir animasyon başladığında kontrol edin. Sunucuda true döndürür.

"use client"

import * as React from "react"
import { IconCheck, IconCopy } from "@tabler/icons-react"

import { useSizeMorph } from "@/lib/motion"

export function MotionSizeMorph() {
  const [copied, setCopied] = React.useState(false)
  const morphRef = useSizeMorph<HTMLButtonElement>({ axis: "width" })

  React.useEffect(() => {
    if (!copied) {
      return
    }
    const timer = setTimeout(() => setCopied(false), 1600)
    return () => clearTimeout(timer)
  }, [copied])

  return (
    <button
      ref={morphRef}
      type="button"
      onClick={() => setCopied(true)}
      className="inline-flex h-9 items-center gap-1.5 overflow-hidden rounded-md bg-secondary px-3 text-sm font-medium whitespace-nowrap outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
    >
      {copied ? (
        <IconCheck className="size-4 shrink-0" />
      ) : (
        <IconCopy className="size-4 shrink-0" />
      )}
      {copied ? "Copied to clipboard" : "Copy"}
    </button>
  )
}
const morphRef = useSizeMorph<HTMLButtonElement>({ axis: "width" })

<button ref={morphRef} className="overflow-hidden whitespace-nowrap">
  {copied ? "Copied to clipboard" : "Copy"}
</button>
  • Öğenin içindeki herhangi bir DOM değişikliği (metin, alt öğeler veya simgeler) bir dönüşümü tetikler. Yeniden boyutlandırma gibi dışarıdan gelen boyut değişiklikleri tetiklemez; böylece öğe kapsayıcısını gecikmesiz izler.
  • Dönüşüm sırasındaki bir değişiklik mevcut boyuttan devam eder. Çalışırken öğede data-morphing bulunur; bunu taşmayı kırpmak veya diğer geçişleri duraklatmak için kullanabilirsiniz.
  • Öğeyi doğal boyutunda tutun: animasyonlu eksende sabit genişlik veya yükseklik vermeyin. Yeni içerik büyürken taşmasın diye overflow-hidden ekleyin.
  • Bir geri çağırma ref'i döndürür. useMergedRef ile diğer ref'lerle birleştirin.
"use client"

import * as React from "react"

import { useSlidingHighlight } from "@/lib/motion"

const views = ["Overview", "Activity", "Settings", "Billing"]

export function MotionSlidingHighlight() {
  const [view, setView] = React.useState(views[0])
  const barRef = React.useRef<HTMLDivElement>(null)
  const highlightRef = React.useRef<HTMLSpanElement>(null)
  useSlidingHighlight(barRef, highlightRef, "[data-active]", "data-active")

  return (
    <div
      ref={barRef}
      role="tablist"
      aria-label="Views"
      className="relative isolate flex rounded-lg bg-muted p-1"
    >
      <span
        ref={highlightRef}
        aria-hidden="true"
        className="pointer-events-none absolute top-0 -z-1 rounded-md bg-background opacity-0 transition-all duration-300 ease-out-quint data-instant:transition-opacity data-visible:opacity-100 motion-reduce:transition-opacity"
      />
      {views.map((item) => (
        <button
          key={item}
          type="button"
          role="tab"
          aria-selected={item === view}
          data-active={item === view ? "" : undefined}
          onClick={() => setView(item)}
          className="h-8 rounded-md px-3 text-sm text-muted-foreground transition-colors duration-150 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden data-active:text-foreground"
        >
          {item}
        </button>
      ))}
    </div>
  )
}
const barRef = React.useRef<HTMLDivElement>(null)
const highlightRef = React.useRef<HTMLSpanElement>(null)
useSlidingHighlight(barRef, highlightRef, "[data-active]", "data-active")

<div ref={barRef} className="relative isolate flex">
  <span
    ref={highlightRef}
    aria-hidden="true"
    className="absolute top-0 -z-1 opacity-0 transition-all duration-300 ease-out-quint data-instant:transition-opacity data-visible:opacity-100"
  />
  {items}
</div>
  • Vurgu satır içi stillerle boyutlandırılır ve kaydırılır. Ona absolute top-0 verin ve transform, width, height ve opacity üzerinde bir geçiş tanımlayın.
  • Hook, adını verdiğiniz niteliği bir MutationObserver ile izler; böylece Base UI'ın kendi data-pressed, data-checked veya aria-current değerleri dahil her yerden gelen durumu takip eder.
  • Bir şey eşleşirken data-visible ayarlanır. Vurgunun zıplaması gerektiğinde data-instant ayarlanır: ilk görünüşte, yeniden boyutlandırmada, kaydırmada ve azaltılmış harekette. Bunu data-instant:transition-opacity olarak biçimlendirin.
  • Çubuğun ölçeğini hesaba katarak ölçüm yapar, bu yüzden hâlâ yakınlaşan bir dialog içinde hizalı kalır.
PropTürVarsayılan
axisHangi boyutun animasyonlu olacağı.
"width" | "height"–
enabledAnimasyon yapılıp yapılmayacağı.
booleantrue
durationMilisaniye.
number300
easingHerhangi bir CSS easing.
stringeaseOut
PropTürVarsayılan
barRefKonumlandırılmış kapsayıcı.
RefObject<HTMLElement | null>–
highlightRefHareket ettirilecek öğe.
RefObject<HTMLElement | null>–
selectorVurgulanacak alt öğeyle eşleşir.
string–
attributeDeğişiklikleri vurguyu hareket ettiren nitelik.
string"data-popup-open"
ExportAçıklama
easeOutcubic-bezier(0.23, 1, 0.32, 1)
easeInOutcubic-bezier(0.77, 0, 0.175, 1)
easeSpringBir linear() yayı.
durationpress, release, hover, enter, exit ve morph.
prefersReducedMotion()Azaltılmış hareketin açık olup olmadığı. Sunucuda true.