Temeller
Hareket
Hareket her bileşenle birlikte tasarlanır. Kısa, amaçlı ve token'lardan gelir; azaltılmış harekette silinmez, yerini sakin bir geçişe bırakır.
İlkeler
Bölüm başlığı “İlkeler”- Amaçlı: Her hareketin tek bir amacı vardır: geri bildirim, durum değişikliği, sistem durumu, mekânsal yön, süreklilik ya da küçük bir işarete dikkat çekmek. Yalnızca süs olan hareket ürün ekranında yer almaz.
- Bileşenle birlikte: Basma, üzerine gelme, odak, yükleniyor, başarı, giriş ve çıkış hareketleri bileşenle aynı anda tasarlanır ve aynı hazır ayarlarla (preset) uygulanır. Düz, tepkisiz bir buton bitmiş sayılmaz.
- Kısa: Etkileşim geri bildirimi 70–110 ms, geçişler 150–400 ms sürer. 400 ms üst sınırdır; kullanıcı hareketin bitmesini beklemez.
- Önce üretken (productive): Arayüzün varsayılanı
productiveeğrilerdir.expressiveeğri ve yaylar yalnızca marka anları içindir ve para akışlarında kullanılmaz. - Azaltılmış hareket değiştirir, silmez: “Hareketi azalt” açıkken ölçek ve kayma kalkar, kısa bir saydamlık geçişi kalır. Bilgi taşıyan hiçbir şey kaybolmaz.
Öğe ne kadar büyükse ve ne kadar uzağa gidiyorsa süre o kadar uzun olur. Oynat düğmesi her süreyi aynı mesafede gösterir.
| Token | Değer | Önizleme |
|---|---|---|
--bigbang-duration-instantBasılı durum, seçim işareti | 70 ms | |
--bigbang-duration-fastÜzerine gelme, odak, renk değişimi, kısa çıkışlar | 110 ms | |
--bigbang-duration-normalAçılır menü, akordeon içeriği, liste girişi | 150 ms | |
--bigbang-duration-slowDiyalog, bildirim, başarı işareti | 240 ms | |
--bigbang-duration-slowerGrafik girişi, sayfa düzeyi geçiş (seyrek), üst sınır | 400 ms |
Hız eğrisi
Bölüm başlığı “Hız eğrisi”Her ailede üç eğri vardır. Standart eğri baştan sona görünen öğeler içindir (yeniden boyutlanan, yer değiştiren). Giriş (-enter) eğrisi hızlı başlar ve yumuşak durur; ekrana gelen öğeler içindir. Çıkış (-exit) eğrisi yavaş başlar ve hızlanarak ayrılır; ekrandan kalıcı olarak çıkan öğeler içindir. linear yalnızca dönen göstergeler ve ilerleme içindir.
| Token | Değer | Önizleme |
|---|---|---|
--bigbang-easing-productiveVarsayılan standart: basma, renk, sekme göstergesi | cubic-bezier(0.2, 0, 0.38, 0.9) | |
--bigbang-easing-expressiveMarka anları; para akışlarında yok | cubic-bezier(0.4, 0.14, 0.3, 1) | |
--bigbang-easing-exit`easing-productive-exit` ile aynı; eski kullanımlar için | cubic-bezier(0.2, 0, 1, 0.9) | |
--bigbang-easing-productive-enterMenü, diyalog, bildirim, liste girişi | cubic-bezier(0, 0, 0.38, 0.9) | |
--bigbang-easing-productive-exitMenü, diyalog, bildirim çıkışı | cubic-bezier(0.2, 0, 1, 0.9) | |
--bigbang-easing-expressive-enterMarka anı girişi | cubic-bezier(0, 0, 0.3, 1) | |
--bigbang-easing-expressive-exitMarka anı çıkışı | cubic-bezier(0.4, 0.14, 1, 1) | |
--bigbang-easing-linearDönen gösterge, iskelet parıltısı, belirsiz ilerleme | cubic-bezier(0, 0, 1, 1) | |
--bigbang-easing-spring-productive-fastYay eğrisi: anahtar düğmesi, basma sonrası geri dönüş | linear(0, 0.034, 0.114, 0.214, 0.319, 0.421, 0.514, 0.597, 0.669, 0.73, 0.781, 0.824, 0.859, 0.887, 0.91, 0.929, 0.944, 0.956, 0.965, 0.973, 0.979, 0.983, 0.987, 0.99, 1) | |
--bigbang-easing-spring-productive-baseYay eğrisi: sekme göstergesi, liste yer değişimi, bildirim yığını | linear(0, 0.034, 0.114, 0.215, 0.32, 0.422, 0.515, 0.598, 0.669, 0.73, 0.781, 0.823, 0.858, 0.886, 0.909, 0.928, 0.943, 0.955, 0.964, 0.972, 0.978, 0.983, 0.986, 0.989, 1) | |
--bigbang-easing-spring-productive-slowYay eğrisi: çekmece (drawer) girişi | linear(0, 0.034, 0.114, 0.214, 0.319, 0.421, 0.513, 0.596, 0.667, 0.728, 0.779, 0.821, 0.856, 0.884, 0.907, 0.926, 0.941, 0.953, 0.963, 0.971, 0.977, 0.982, 0.986, 0.989, 1) | |
--bigbang-easing-spring-expressive-baseYay eğrisi: marka anı, hafif taşma | linear(0, 0.031, 0.108, 0.21, 0.322, 0.435, 0.542, 0.638, 0.722, 0.793, 0.851, 0.898, 0.934, 0.962, 0.982, 0.997, 1.006, 1.012, 1.015, 1.016, 1.016, 1.015, 1.013, 1.012, 1) |
Yaylar
Bölüm başlığı “Yaylar”Yay (spring), süre yerine fiziksel değerlerle tanımlanır: sertlik (stiffness), sönüm (damping) ve kütle (mass). Değerler DTCG $extensions alanında durur; derleme her yay için oturma süresini (hedefin %1 yakınına geldiği an) doğrular ve buna uyan bir CSS linear() eğrisi üretir. CSS’te süre ile eğri birlikte kullanılır: --bigbang-spring-productive-fast ve --bigbang-easing-spring-productive-fast. JavaScript motorları (Motion, React Native Reanimated) aynı sayıları motionValues.spring üzerinden alır.
| Token | Açık mod | Koyu mod |
|---|---|---|
--bigbang-spring-productive-fastSertlik 900, sönüm 60 · taşma yok | 0.23s0.23s0.23s | 0.23s0.23s0.23s |
--bigbang-spring-productive-baseSertlik 500, sönüm 45 · taşma yok | 0.31s0.31s0.31s | 0.31s0.31s0.31s |
--bigbang-spring-productive-slowSertlik 300, sönüm 35 · taşma yok | 0.4s0.4s0.4s | 0.4s0.4s0.4s |
--bigbang-spring-expressive-baseSertlik 380, sönüm 31 · hafif taşma, yalnızca marka anları | 0.33s0.33s0.33s | 0.33s0.33s0.33s |
Ne zaman yay, ne zaman Bézier? Kullanıcının elinden çıkan ya da kesilebilen hareketlerde yay kullanın: basmadan sonra geri dönüş, anahtar düğmesi, sekme göstergesi, sıralama, çekmece. Hareket ortasında hedef değişirse yay doğal görünür. Saydamlık, renk ve sabit süresi olan giriş/çıkışlarda Bézier eğri kullanın. Ürün ekranındaki yaylar kritik sönümlüdür (taşma yapmaz).
Anlamsal hareket token’ları
Bölüm başlığı “Anlamsal hareket token’ları”Bileşenler süre ve eğriyi doğrudan değil, kalıba göre adlanmış token’lardan alır: --bigbang-motion-<kalıp>-<evre>-<özellik>. Örneğin motion-popover-enter-duration, motion-dialog-scale, motion-list-stagger. Bir kalıbın hızı değişecekse yalnızca bu token değişir.
| Token | Açık mod | Koyu mod |
|---|---|---|
--bigbang-motion-press-duration | 0.07s0.07s0.07s | 0.07s0.07s0.07s |
--bigbang-motion-press-easing | cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9) | cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9) |
--bigbang-motion-press-scale | 0.970.970.97 | 0.970.970.97 |
--bigbang-motion-release-duration | 0.23s0.23s0.23s | 0.23s0.23s0.23s |
--bigbang-motion-glass-press-scale | 1.021.021.02 | 1.021.021.02 |
--bigbang-motion-hover-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-hover-easing | cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9) | cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9) |
--bigbang-motion-hover-lift | -2px-2px-2px | -2px-2px-2px |
--bigbang-motion-focus-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-focus-easing | cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9) | cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9)cubic-bezier(0.2, 0, 0.38, 0.9) |
--bigbang-motion-feedback-duration | 0.24s0.24s0.24s | 0.24s0.24s0.24s |
--bigbang-motion-feedback-easing | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) |
--bigbang-motion-feedback-scale | 0.60.60.6 | 0.60.60.6 |
--bigbang-motion-feedback-hold | 2s2s2s | 2s2s2s |
--bigbang-motion-selection-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-selection-easing | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) |
--bigbang-motion-selection-scale | 0.60.60.6 | 0.60.60.6 |
--bigbang-motion-switch-duration | 0.23s0.23s0.23s | 0.23s0.23s0.23s |
--bigbang-motion-popover-enter-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-popover-exit-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-popover-enter-easing | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) |
--bigbang-motion-popover-exit-easing | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) |
--bigbang-motion-popover-scale | 0.960.960.96 | 0.960.960.96 |
--bigbang-motion-popover-distance | 4px4px4px | 4px4px4px |
--bigbang-motion-tooltip-delay | 0.4s0.4s0.4s | 0.4s0.4s0.4s |
--bigbang-motion-tooltip-enter-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-tooltip-exit-duration | 0.07s0.07s0.07s | 0.07s0.07s0.07s |
--bigbang-motion-tooltip-distance | 2px2px2px | 2px2px2px |
--bigbang-motion-dialog-enter-duration | 0.24s0.24s0.24s | 0.24s0.24s0.24s |
--bigbang-motion-dialog-exit-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-dialog-enter-easing | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) |
--bigbang-motion-dialog-exit-easing | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) |
--bigbang-motion-dialog-scale | 0.960.960.96 | 0.960.960.96 |
--bigbang-motion-dialog-distance | 8px8px8px | 8px8px8px |
--bigbang-motion-drawer-enter-duration | 0.4s0.4s0.4s | 0.4s0.4s0.4s |
--bigbang-motion-drawer-exit-duration | 0.24s0.24s0.24s | 0.24s0.24s0.24s |
--bigbang-motion-drawer-exit-easing | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) |
--bigbang-motion-drawer-distance | 100%100%100% | 100%100%100% |
--bigbang-motion-toast-enter-duration | 0.24s0.24s0.24s | 0.24s0.24s0.24s |
--bigbang-motion-toast-exit-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-toast-enter-easing | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) |
--bigbang-motion-toast-exit-easing | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) | cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9)cubic-bezier(0.2, 0, 1, 0.9) |
--bigbang-motion-toast-distance | 8px8px8px | 8px8px8px |
--bigbang-motion-collapsible-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-collapsible-easing | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) | cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9)cubic-bezier(0, 0, 0.38, 0.9) |
--bigbang-motion-collapsible-distance | 4px4px4px | 4px4px4px |
--bigbang-motion-collapsible-icon-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-indicator-duration | 0.31s0.31s0.31s | 0.31s0.31s0.31s |
--bigbang-motion-skeleton-reveal-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-route-enter-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-route-exit-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-route-shared-duration | 0.24s0.24s0.24s | 0.24s0.24s0.24s |
--bigbang-motion-list-enter-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-list-exit-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-list-move-duration | 0.31s0.31s0.31s | 0.31s0.31s0.31s |
--bigbang-motion-list-distance | 4px4px4px | 4px4px4px |
--bigbang-motion-list-stagger | 0.03s0.03s0.03s | 0.03s0.03s0.03s |
--bigbang-motion-value-duration | 0.11s0.11s0.11s | 0.11s0.11s0.11s |
--bigbang-motion-progress-duration | 0.15s0.15s0.15s | 0.15s0.15s0.15s |
--bigbang-motion-chart-enter-duration | 0.4s0.4s0.4s | 0.4s0.4s0.4s |
--bigbang-motion-chart-update-duration | 0.24s0.24s0.24s | 0.24s0.24s0.24s |
Durum katmanı
Bölüm başlığı “Durum katmanı”Kendi üzerine gelme rengi olmayan yüzeylerde (kart, liste satırı, ikon buton) etkileşim, metin renginde ince bir katmanla gösterilir. Opaklıklar:
| Token | Açık mod | Koyu mod |
|---|---|---|
--bigbang-state-layer-hoveredÜzerine gelindiğinde | 0.080.080.08 | 0.080.080.08 |
--bigbang-state-layer-focusedKlavye odağında (halka ile birlikte) | 0.10.10.1 | 0.10.10.1 |
--bigbang-state-layer-pressedBasılıyken | 0.10.10.1 | 0.10.10.1 |
--bigbang-state-layer-draggedSürüklenirken | 0.160.160.16 | 0.160.160.16 |
Mikro etkileşimler
Bölüm başlığı “Mikro etkileşimler”Aşağıdaki örnekler @bigbang/ui paketindeki motion hazır ayarlarını kullanır. Bileşenler aynı sınıfları kendi stil tariflerinde kullanır; prop değişmez.
Basma ve üzerine gelme
Bölüm başlığı “Basma ve üzerine gelme”Butonlar basılıyken 70 ms’de %97’ye küçülür ve bırakınca hızlı yayla döner. İkon butonlar ve kartlar durum katmanı kullanır; tıklanabilir kart üzerine gelindiğinde 2 px yükselir.
import { motion } from '@bigbang/ui';import { primaryButton, secondaryButton } from '../demo';
export default function Press() { return ( <div className="flex flex-wrap items-center gap-lg"> <button type="button" className={primaryButton}> Onayla </button> <button type="button" className={secondaryButton}> Vazgeç </button> <button type="button" aria-label="Diğer seçenekler" className={`${motion.press} ${motion.stateLayer} inline-flex size-(--bigbang-button-height-md) items-center justify-center rounded-full text-fg-default outline-none focus-visible:focus-outline`} > <svg aria-hidden="true" viewBox="0 0 16 16" fill="currentColor" className="size-4"> <circle cx="3" cy="8" r="1.5" /> <circle cx="8" cy="8" r="1.5" /> <circle cx="13" cy="8" r="1.5" /> </svg> </button> <button type="button" className={`${motion.stateLayer} ${motion.lift} flex w-56 flex-col items-start gap-xs rounded-lg border border-line-subtle bg-surface-raised p-lg text-start outline-none focus-visible:focus-outline`} > <span className="type-label-md text-fg-default">Kart başlığı</span> <span className="type-body-sm text-fg-subtle">Kart açıklaması</span> </button> </div> );}Yükleniyor ve başarı
Bölüm başlığı “Yükleniyor ve başarı”İşlem sürerken etiket yerinde kalır (genişlik değişmez), üstünde dönen gösterge belirir. Başarı işareti %60’tan büyüyerek gelir, iki saniye kalır ve tekrar etmez. Sonuç ekran okuyucuya ayrıca duyurulur.
import { motion, motionValues } from '@bigbang/ui';import { useEffect, useState } from 'react';import { primaryButton } from '../demo';
type Step = 'idle' | 'busy' | 'done';
export default function Feedback() { const [step, setStep] = useState<Step>('idle'); useEffect(() => { // The first timer stands in for the request; the second returns the button to rest. const next = step === 'busy' ? 'done' : step === 'done' ? 'idle' : null; if (!next) return undefined; const id = setTimeout(() => setStep(next), step === 'busy' ? 1200 : motionValues.motion['feedback-hold']); return () => clearTimeout(id); }, [step]); return ( <div className="flex items-center gap-lg"> <button type="button" aria-disabled={step !== 'idle'} aria-busy={step === 'busy'} onClick={() => step === 'idle' && setStep('busy')} className={`${primaryButton} relative min-w-40`} > <span className={step === 'idle' ? '' : 'opacity-0'}>Kaydet</span> {step === 'busy' ? ( <span className="absolute inset-0 flex items-center justify-center"> <svg aria-hidden="true" viewBox="0 0 16 16" fill="none" className="size-4 animate-spin reduce-motion:animate-none"> <circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeOpacity="0.3" strokeWidth="2" /> <path d="M14.5 8A6.5 6.5 0 0 0 8 1.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /> </svg> </span> ) : null} {step === 'done' ? ( <span className={`${motion.success} absolute inset-0 flex items-center justify-center gap-xs`}> <svg aria-hidden="true" viewBox="0 0 16 16" fill="none" className="size-4"> <path d="M3 8.5l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> </svg> Kaydedildi </span> ) : null} </button> <span role="status" className="sr-only"> {step === 'done' ? 'Kaydedildi' : ''} </span> </div> );}Alan odağı ve hata
Bölüm başlığı “Alan odağı ve hata”Kenarlık rengi 110 ms’de değişir; odak halkası gecikmesiz görünür. Hata mesajı saydamlaşarak 4 px aşağı iner. Alan sallanmaz.
import { motion } from '@bigbang/ui';import { useId, useState } from 'react';import { secondaryButton } from '../demo';
export default function Field() { const id = useId(); const [value, setValue] = useState(''); const [invalid, setInvalid] = useState(false); return ( <form noValidate onSubmit={(event) => { event.preventDefault(); setInvalid(value.trim() === ''); }} className="flex w-full max-w-sm flex-col items-start gap-sm" > <label htmlFor={id} className="type-label-md text-fg-default"> Alan etiketi </label> <input id={id} value={value} onChange={(event) => setValue(event.target.value)} aria-invalid={invalid || undefined} aria-describedby={invalid ? `${id}-error` : undefined} className={`${motion.field} h-(--bigbang-input-height-md) w-full rounded-(--bigbang-input-radius) border border-line-input bg-surface-raised px-(--bigbang-input-padding-inline) type-body-md text-fg-default outline-none focus-visible:border-line-focus focus-visible:focus-outline-inset aria-invalid:border-line-danger`} /> {invalid ? ( <p id={`${id}-error`} className={`${motion.fieldMessage} type-body-sm text-fg-danger`}> Hata mesajı burada yer alır. </p> ) : null} <button type="submit" className={secondaryButton}> Kontrol et </button> </form> );}Onay kutusu ve anahtar
Bölüm başlığı “Onay kutusu ve anahtar”Onay işareti %60’tan büyüyerek gelir. Anahtar düğmesi hızlı yayla kayar; azaltılmış harekette yer değiştirmesi anlık olur, renk geçişi kalır.
import { motion } from '@bigbang/ui';import { useState } from 'react';
const row = 'flex items-center gap-md type-body-md text-fg-default outline-none focus-visible:focus-outline rounded-xs';const state = (on: boolean) => (on ? { 'data-checked': '' } : { 'data-unchecked': '' });
export default function Selection() { const [checked, setChecked] = useState(true); const [on, setOn] = useState(false); return ( <div className="flex flex-col items-start gap-lg"> <button type="button" role="checkbox" aria-checked={checked} onClick={() => setChecked(!checked)} className={row}> <span className={`${motion.press} ${motion.hover} flex size-5 items-center justify-center rounded-xs border ${checked ? 'border-transparent bg-action-primary-default text-action-on-primary-default' : 'border-line-input bg-surface-raised'}`} > <svg aria-hidden="true" viewBox="0 0 16 16" fill="none" {...state(checked)} className={`${motion.selectionMark} size-3.5`}> <path d="M3 8.5l3 3 7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> </svg> </span> Seçenek etiketi </button> <button type="button" role="switch" aria-checked={on} onClick={() => setOn(!on)} className={row}> <span className={`${motion.hover} relative h-6 w-10 rounded-full [--switch-travel:16px] ${on ? 'bg-action-primary-default' : 'bg-line-input'}`} > <span {...state(on)} className={`${motion.switchThumb} absolute start-0.5 top-0.5 size-5 rounded-full bg-surface-raised shadow-raised`} /> </span> Örnek ayar </button> </div> );}Geçişler
Bölüm başlığı “Geçişler”Menü, açılır pencere ve diyalog
Bölüm başlığı “Menü, açılır pencere ve diyalog”Menü bağlı olduğu öğeden %96 ölçek ve 4 px ile açılır (150 ms), daha hızlı kapanır (110 ms). Diyalog 8 px yükselerek 240 ms’de gelir; perde aynı hızda koyulaşır. İpucu 400 ms bekledikten sonra 110 ms’de görünür.
import { motion } from '@bigbang/ui';import { useId, useState } from 'react';import { panel, primaryButton, secondaryButton, stage } from '../demo';
// Demo only: shows the motion. Real overlays come from Base UI parts, which also manage focus and Escape.const visibility = (open: boolean) => (open ? { 'data-open': '' } : { 'data-closed': '', inert: true });
export default function Overlays() { const id = useId(); const [menu, setMenu] = useState(false); const [dialog, setDialog] = useState(false); return ( <div className={`${stage} min-h-64 p-xl`}> <div className="flex gap-lg"> <div className="relative"> <button type="button" aria-expanded={menu} aria-controls={`${id}-menu`} onClick={() => setMenu(!menu)} className={secondaryButton}> Eylemler </button> <div id={`${id}-menu`} {...visibility(menu)} className={`${motion.popover} absolute start-0 top-full z-10 mt-xs flex w-48 flex-col rounded-md border border-line-subtle bg-surface-raised p-xs shadow-overlay`} > {['İndir', 'Paylaş', 'Arşivle'].map((item) => ( <button key={item} type="button" onClick={() => setMenu(false)} className={`${motion.hover} rounded-sm px-md py-sm text-start type-body-md text-fg-default hover:bg-surface-hovered`}> {item} </button> ))} </div> </div> <button type="button" onClick={() => setDialog(true)} className={primaryButton}> Diyaloğu aç </button> </div> <div {...visibility(dialog)} onClick={() => setDialog(false)} className={`${motion.scrim} absolute inset-0 z-20 rounded-md bg-surface-scrim`} /> <div role="dialog" aria-labelledby={`${id}-title`} {...visibility(dialog)} className={`${motion.dialog} ${panel} absolute inset-x-xl top-xl z-30 flex flex-col items-start gap-md`} > <h3 id={`${id}-title`} className="m-0 type-heading-sm"> Diyalog başlığı </h3> <p className="m-0 type-body-md text-fg-subtle">Diyalog açıklaması burada yer alır.</p> <button type="button" onClick={() => setDialog(false)} className={secondaryButton}> Kapat </button> </div> </div> );}Bildirim (toast)
Bölüm başlığı “Bildirim (toast)”Bildirim 8 px yükselerek gelir, yığın orta yayla kayar. Kendiliğinden kapanan bildirimler en az 5 saniye kalır ve üzerine gelindiğinde ya da odaklanıldığında bekler.
import { motion, motionValues } from '@bigbang/ui';import { useState } from 'react';import { primaryButton, stage } from '../demo';
interface Item { id: number; leaving?: boolean;}
export default function Toast() { const [items, setItems] = useState<Item[]>([]); const dismiss = (id: number) => { setItems((current) => current.map((item) => (item.id === id ? { ...item, leaving: true } : item))); setTimeout(() => setItems((current) => current.filter((item) => item.id !== id)), motionValues.motion['toast-exit-duration']); }; return ( <div className={`${stage} min-h-56`}> <button type="button" onClick={() => setItems((current) => [...current.slice(-2), { id: Date.now() }])} className={primaryButton}> Bildirim göster </button> <ol aria-live="polite" className="absolute end-0 bottom-0 m-0 flex w-72 list-none flex-col gap-sm p-0"> {items.map((item) => ( <li key={item.id} {...(item.leaving ? { 'data-ending-style': '' } : {})} className={`${motion.toast} flex items-center justify-between gap-md rounded-md bg-surface-inverse px-lg py-md type-body-md text-fg-inverse shadow-overlay`} > Bildirim metni <button type="button" onClick={() => dismiss(item.id)} className={`${motion.press} rounded-sm px-sm type-label-sm text-fg-inverse underline`}> Kapat </button> </li> ))} </ol> </div> );}Sekme göstergesi ve akordeon
Bölüm başlığı “Sekme göstergesi ve akordeon”Gösterge etkin sekmeyi orta yayla izler. Akordeon panelinin yüksekliği anında değişir; içerik saydamlaşarak 4 px iner, ok yarım tur döner. Yükseklik canlandırılmaz, çünkü yerleşim (layout) canlandırmak ana iş parçacığını yorar.
import { motion } from '@bigbang/ui';import { type CSSProperties, useId, useLayoutEffect, useRef, useState } from 'react';
export default function Disclosure() { const tabs = ['Birinci sekme', 'İkinci sekme', 'Üçüncü sekme']; const id = useId(); const [active, setActive] = useState(0); const [open, setOpen] = useState(false); const [bar, setBar] = useState({ left: 0, width: 0 }); const listRef = useRef<HTMLDivElement>(null); useLayoutEffect(() => { const tab = listRef.current?.children[active] as HTMLElement | undefined; if (tab) setBar({ left: tab.offsetLeft, width: tab.offsetWidth }); }, [active]); return ( <div className="flex w-full max-w-md flex-col gap-xl"> <div className="relative border-b border-line-subtle"> <div ref={listRef} role="tablist" aria-label="Bölümler" className="flex"> {tabs.map((tab, index) => ( <button key={tab} type="button" role="tab" aria-selected={index === active} onClick={() => setActive(index)} className={`${motion.hover} px-lg py-sm type-label-md outline-none focus-visible:focus-outline-inset ${index === active ? 'text-fg-default' : 'text-fg-subtle hover:text-fg-default'}`} > {tab} </button> ))} </div> <span aria-hidden="true" style={{ '--active-tab-left': `${bar.left}px`, '--active-tab-width': `${bar.width}px` } as CSSProperties} className={`${motion.indicator} start-0 -bottom-px h-0.5 rounded-full bg-action-primary-default`} /> </div> <div className="group flex flex-col" {...(open ? { 'data-open': '' } : {})}> <button type="button" aria-expanded={open} aria-controls={`${id}-panel`} onClick={() => setOpen(!open)} className={`${motion.hover} flex items-center justify-between rounded-sm py-sm type-label-md text-fg-default outline-none focus-visible:focus-outline`} > Ayrıntılar <svg aria-hidden="true" viewBox="0 0 16 16" fill="none" className={`${motion.collapsibleIcon} size-4`}> <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> </button> {open ? ( <p id={`${id}-panel`} className={`${motion.collapsible} m-0 type-body-md text-fg-subtle`}> Açılan içerik burada yer alır. </p> ) : null} </div> </div> );}İskeletten içeriğe
Bölüm başlığı “İskeletten içeriğe”İskelet parıltısı üç kez geçer (4,5 sn) ve durur. İçerik geldiğinde 110 ms’de belirir. Azaltılmış harekette iskelet durağandır.
import { motion } from '@bigbang/ui';import { useEffect, useState } from 'react';import { secondaryButton } from '../demo';
export default function Skeleton() { const [loading, setLoading] = useState(true); useEffect(() => { if (!loading) return undefined; // Stands in for the request. const id = setTimeout(() => setLoading(false), 1800); return () => clearTimeout(id); }, [loading]); return ( <div className="flex w-full max-w-md flex-col items-start gap-lg"> <button type="button" onClick={() => setLoading(true)} className={secondaryButton}> Yenile </button> <div aria-busy={loading} className="flex w-full flex-col gap-sm rounded-lg border border-line-subtle bg-surface-raised p-lg"> {loading ? ( <> <span className={`${motion.skeleton} block h-5 w-1/2 rounded-sm`} /> <span className={`${motion.skeleton} block h-4 w-full rounded-sm`} /> <span className={`${motion.skeleton} block h-4 w-3/4 rounded-sm`} /> </> ) : ( <div className={`${motion.reveal} flex flex-col gap-sm`}> <span className="type-heading-sm text-fg-default">Kart başlığı</span> <span className="type-body-md text-fg-subtle">Kart açıklaması burada yer alır.</span> </div> )} </div> </div> );}Liste ekleme, çıkarma ve kademeli giriş
Bölüm başlığı “Liste ekleme, çıkarma ve kademeli giriş”Yeni öğe 4 px kayarak 150 ms’de gelir; çıkan öğe 110 ms’de solar. Kademeli girişte öğeler arası 30 ms beklenir; sıra 8. öğeden sonra beklemez, böylece bir grup 400 ms’yi geçmez.
- Birinci öğe
- İkinci öğe
- Üçüncü öğe
import { motion, motionValues } from '@bigbang/ui';import { type CSSProperties, useState } from 'react';import { secondaryButton } from '../demo';
interface Row { id: number; label: string; leaving?: boolean;}
export default function List() { const [rows, setRows] = useState<Row[]>(() => ['Birinci öğe', 'İkinci öğe', 'Üçüncü öğe'].map((label, id) => ({ id, label }))); const [round, setRound] = useState(0); const add = () => setRows((current) => [{ id: Date.now(), label: 'Yeni öğe' }, ...current]); const remove = (id: number) => { // Keep the row while it fades out, then drop it. setRows((current) => current.map((row) => (row.id === id ? { ...row, leaving: true } : row))); setTimeout(() => setRows((current) => current.filter((row) => row.id !== id)), motionValues.motion['list-exit-duration']); }; return ( <div className="flex w-full max-w-md flex-col gap-md"> <div className="flex gap-md"> <button type="button" onClick={add} className={secondaryButton}> Ekle </button> <button type="button" onClick={() => setRound(round + 1)} className={secondaryButton}> Yeniden oynat </button> </div> <ul key={round} className="m-0 flex list-none flex-col gap-xs p-0"> {rows.map((row, index) => ( <li key={row.id} {...(row.leaving ? { 'data-ending-style': '' } : {})} style={{ '--stagger-index': index } as CSSProperties} className={`${motion.listItem} flex items-center justify-between rounded-md border border-line-subtle bg-surface-raised px-lg py-sm type-body-md text-fg-default`} > {row.label} <button type="button" onClick={() => remove(row.id)} className={`${motion.hover} rounded-sm px-sm type-label-sm text-fg-subtle hover:bg-surface-hovered`}> Kaldır </button> </li> ))} </ul> </div> );}Tutar ve ilerleme
Bölüm başlığı “Tutar ve ilerleme”Değişen tutar 110 ms’de yer değiştirerek belirir. Tutarlar sayarak artmaz ya da yuvarlanmaz: ara değerler yanlış bir tutar gösterir. İlerleme çubuğunun dolgusu translate ile kayar, böylece uçları her değerde yuvarlak kalır; ölçer ve diğer dolgular scale-x ile büyür.
import { motion } from '@bigbang/ui';import { type CSSProperties, useState } from 'react';import { secondaryButton } from '../demo';
const limit = 100;
export default function Value() { const number = new Intl.NumberFormat('tr-TR'); const [total, setTotal] = useState(24); return ( <div className="flex w-full max-w-md flex-col items-start gap-lg"> <div className="flex flex-col gap-xs"> <span className="type-body-sm text-fg-subtle">Değer</span> {/* A new key remounts the figure, so it cross-fades. Values never count up or roll. */} <span key={total} className={`${motion.reveal} type-heading-lg tabular-nums text-fg-default`}> {number.format(total)} </span> </div> <div className="flex w-full flex-col gap-xs"> <div role="progressbar" aria-label="İlerleme" aria-valuemin={0} aria-valuemax={limit} aria-valuenow={Math.min(total, limit)} className="h-2 w-full overflow-hidden rounded-full bg-surface-sunken" > <div style={{ '--progress': Math.min(total / limit, 1) } as CSSProperties} className={`${motion.progress} h-full w-full rounded-full bg-action-primary-default`} /> </div> <span className="type-body-sm text-fg-subtle">İlerleme</span> </div> <button type="button" onClick={() => setTotal(Math.min(total + 8, limit))} className={secondaryButton}> Değeri artır </button> </div> );}Sayfa geçişi ve ortak öğe
Bölüm başlığı “Sayfa geçişi ve ortak öğe”Sayfa değişiminde yalnızca içerik alanı değişir: eski içerik 110 ms’de solar, yenisi 150 ms’de belirir; kabuk (başlık, yan menü) yerinde kalır. Her sayfa geçişinde “her şey yukarı kayarak gelir” yapılmaz. Liste satırından ayrıntı sayfasına geçişte satırın başlığı ortak öğe olarak 240 ms’de yerine gider (View Transitions API). Desteklemeyen tarayıcıda geçiş anlık olur.
Grafikler
Bölüm başlığı “Grafikler”Grafik ilk açılışta 400 ms’de çizilir, veri güncellenince 240 ms’de geçer. Filtre ya da sayfalama sonrasında grafik baştan çizilmez. ECharts’ta animationDuration ve animationDurationUpdate değerleri motionValues.motion üzerinden alınır; azaltılmış harekette animation: false olur.
Koreografi
Bölüm başlığı “Koreografi”- Bir ekranda en fazla 3 kademe grubu olur, grup içinde öğeler arası en fazla 40 ms (varsayılan 30 ms) beklenir, toplam 400 ms’yi geçmez.
- Sıra: önce kap (panel, diyalog), sonra içerik. İçerik kabı beklemez; aynı anda başlar, yalnızca gecikmeyle ayrışır.
- Giriş hareketi yalnızca içeriğin ilk görünüşündedir; yenileme, filtre ve sayfalamada tekrar etmez.
- Aynı anda en fazla bir büyük öğe hareket eder (diyalog, çekmece ya da sayfa içeriği).
Azaltılmış hareket
Bölüm başlığı “Azaltılmış hareket”İşletim sistemindeki “hareketi azalt” ayarı ya da ürün içindeki data-motion="reduce" geçersiz kılması token’ları değiştirir: ölçekler 1, mesafeler 0 olur, uzun soluklar 110 ms’ye iner. Bileşen kodunda ayrı bir dal gerekmez. Döngüler (skeleton, indeterminate) reduce-motion: varyantıyla durur.
| Öğe | Tam hareket | Azaltılmış harekette |
|---|---|---|
| Basma | %97 ölçek, yay ile dönüş | Yalnız renk değişimi |
| Kart üzerine gelme | 2 px yükselme | Yalnız kenarlık rengi |
| Başarı işareti | %60’tan büyüme, 240 ms | 110 ms belirme, durağan |
| Menü, ipucu | Ölçek + kayma | 110 ms belirme |
| Diyalog, çekmece | Ölçek + kayma / tam genişlik kayma | 110 ms belirme |
| Bildirim, liste | 8 / 4 px kayma, kademe | Belirme, kademe yok |
| Anahtar, sekme göstergesi | Yay ile kayma | Anlık yer değişimi, renk geçişi kalır |
| İskelet, belirsiz ilerleme | Parıltı / kayan çubuk | Durağan blok / tam çubuk |
| Dönen gösterge | Döner | Durağan simge ve metin |
| Grafik | Çizilerek gelir | Anında |
| Marka animasyonu (Rive) | Bir kez oynar | Son kare durağan gösterilir |
import { motion } from '@bigbang/ui';import { useState } from 'react';import { panel, primaryButton, secondaryButton } from '../demo';
const visibility = (open: boolean) => (open ? { 'data-open': '' } : { 'data-closed': '', inert: true });
export default function Reduced() { const [reduce, setReduce] = useState(true); const [open, setOpen] = useState(false); return ( // data-motion="reduce" applies the same token replacements as the operating-system setting. <div {...(reduce ? { 'data-motion': 'reduce' } : {})} className="relative flex w-full flex-col items-start gap-lg"> <label className="flex items-center gap-sm type-body-md text-fg-default"> <input type="checkbox" checked={reduce} onChange={() => setReduce(!reduce)} className="size-4 accent-(--bigbang-color-action-primary-default)" /> Hareketi azalt </label> <div className="flex gap-lg"> <button type="button" onClick={() => setOpen(!open)} className={primaryButton}> {open ? 'Paneli gizle' : 'Paneli göster'} </button> <button type="button" className={secondaryButton}> Bana bas </button> </div> <div {...visibility(open)} className={`${motion.dialog} ${panel} w-full max-w-sm type-body-md`}> Hareket azaltıldığında bu panel yalnızca belirir; hiçbir şey büyümez ya da kaymaz. </div> </div> );}Para akışları
Bölüm başlığı “Para akışları”Ödeme, transfer, iade ve limit gibi S1/S2 akışlarında yalnızca productive eğri ve kritik sönümlü yaylar kullanılır. Bu akışlarda kutlama animasyonu, taşan yay, sallanma, sayan ya da yuvarlanan tutar yoktur. Onay ekranında başarı işareti bir kez gelir ve durağan kalır. Bir hata, alanın rengi, simgesi ve metniyle anlatılır.
Performans
Bölüm başlığı “Performans”- Yalnızca
opacity,transform(scale,translate,rotate) ve renk canlandırılır. Sekme göstergesinin genişliği tek istisnadır.width,height,top,left,margincanlandırılmaz. will-changeyalnızca hareket süresince eklenir. Görünmeyen canvas animasyonu durur.- Etkileşimden sonraki boyama (INP) 75. yüzdelikte 200 ms’yi geçmez; hareket sırasında 50 ms’den uzun görev olmaz.
- Ürün rotasında hareket için en fazla 30 kB (gzip) JavaScript yüklenir; Rive yalnızca kullanıldığı rotada, sonradan yüklenir.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- 5 saniyeden uzun ve kendiliğinden başlayan hareket durdurulabilir (WCAG 2.2.2). Sonsuz döngü yalnızca dönen göstergede vardır.
- Saniyede üçten fazla yanıp sönme yoktur (WCAG 2.3.1).
- Etkileşimle başlayan hareket kapatılabilir (WCAG 2.3.3, AAA hedefi): azaltılmış hareket ayarı ve ürün içi geçersiz kılma bunu sağlar.
- Hareket bilgiyi tek başına taşımaz; durum metinle ve ekran okuyucu duyurusuyla da verilir.
Motorlar
Bölüm başlığı “Motorlar”| İş | Araç |
|---|---|
| Basma, üzerine gelme, odak, renk, giriş/çıkış, iskelet | CSS ve hareket token’ları (varsayılan) |
| Sayfa geçişi, ortak öğe | View Transitions API (aynı belge) |
| Kesilebilir yay, sürükleme, yerleşim (layout) canlandırması | Motion; ADR sonrası |
| İllüstrasyon ve marka animasyonu | Rive; ADR sonrası, yalnız vektör dosya, kendi sunucumuzdaki WASM |
| Hazır vektör animasyonu | dotLottie; ADR sonrası, kendi sunucumuzdaki WASM ve worker |
| lottie-web | Kullanılmaz (eval gerektirir) |
Gözden geçirme listesi
Bölüm başlığı “Gözden geçirme listesi”- Her hareketin amacı yazılı mı (hikâyede
motion.purpose)? - Süre, eğri ve mesafe yalnızca token’dan mı geliyor? Ham
ms,cubic-bezier()yok mu? - Yalnızca saydamlık, dönüşüm ve renk mi canlanıyor?
- Azaltılmış harekette yerine ne geliyor? 0,25× hızda kayıt incelendi mi?
- Para akışında
expressiveeğri ya da taşan yay var mı? - 5 saniyeyi geçen ya da döngüye giren bir şey var mı; durdurulabiliyor mu?
- Giriş hareketi yenilemede ya da filtrede tekrar ediyor mu?
Yönergeler
Bölüm başlığı “Yönergeler”Token kullanın, azaltılmış hareketi token’lara bırakın
Bölüm başlığı “Token kullanın, azaltılmış hareketi token’lara bırakın”tv({ base: [motion.press, ‘…’] })duration-700 ease-in-out hover:scale-110Tutarları saydırmayın
Bölüm başlığı “Tutarları saydırmayın”<span key={total} className={motion.reveal}>countUp(0, balance, 1200)