Vercel’in UI SDK’sını Tailwind ile stillendirmek: data-slot, durum öznitelikleri ve giriş/çıkış animasyonları
Vercel’in yeni UI SDK’sını Tailwind v4 ve shadcn token’larıyla stillendirin: data-slot, durum öznitelikleri, CSS değişkenleri, giriş ve çıkış animasyonları.
Preet Suthar · · 9 dk okuma
- React
- UI SDK
- Tailwind CSS
- shadcn/ui
Vercel Labs, UI SDK’yı 12 Kasım 2026’da npm’de yayınladı. React, Vue, Svelte, Solid ve Web Components için, davranışlardan oluşan bir çekirdek kütüphane üzerine kurulmuş stilsiz, erişilebilir bileşenler seti. Bileşenler odağı, klavyeyi ve ARIA’yı yönetir. Nasıl göründükleri sizin işiniz.
Bu yazı dördünü Tailwind CSS v4 ve shadcn tarzı tema token’larıyla stillendiriyor: bir Select, bir Popover, bir Rating ve bir Color Picker. Her biri farklı bir stil kancası gösteriyor. Aşağıdaki her kod parçası Vite 8, React 19.3, Tailwind CSS 4.3 ve 0.1.0-beta.1 sürümündeki @ui-sdk/react ile @ui-sdk/core kullanılarak kurulmuş bir deneme uygulamasında çalışıyor. UI SDK beta aşamasında ve dokümanları API’lerin hâlâ değişebileceğini söylüyor, bu yüzden kurduğunuz sürümü kontrol edin.
UI SDK’da stil nasıl çalışır
UI SDK’nın üç katmanı var. @ui-sdk/core, roving focus, typeahead ve açılır öğeler gibi davranışları barındırır. definePrimitive ile yazılan bir primitive, davranışları durum ve adlandırılmış slot’larla birleştirir. @ui-sdk/react gibi bir framework adaptörü her slot’u SelectTrigger ya da RatingItem gibi import ettiğiniz bir parça olarak render eder.
- Davranışlar
@ui-sdk/coreOdak, tuşlar, seçim, açılır öğeler, sürükle ve bırak. - Primitive
definePrimitiveDurum, bir API ve adlandırılmış slot’lar tek bir tanımda. - Parçalar
@ui-sdk/reactHer slotdata-slot, durum öznitelikleri ve CSS değişkenleriyle render edilir. - Sizin stilleriniz
classNameBu kancaları hedefleyen Tailwind yardımcı sınıfları ya da düz CSS.
<div data-slot="select-item" data-highlighted data-selected role="option">
className="data-highlighted:bg-accent data-selected:font-medium"
Stilleriniz parçalara üç kanca üzerinden bağlanır:
data-slot: her parçada bir tane vardır, örneğindata-slot="select-item". Parçayı global CSS ve ajanlar için adlandırır.- Durum öznitelikleri:
data-open,data-highlighted,data-selected,data-disabledve benzerleri. Çoğu ya vardır ya yoktur. Bazıları bir değer taşır, örneğindata-side="top"ya dadata-orientation="vertical". - CSS değişkenleri: bir sınıfın ifade edemediği değerler; örneğin açılır öğedeki
--anchor-widthya da yıldızdaki--rating-fill.
className ve style, Base UI’ın izin verdiği gibi parçanın durumunun fonksiyonu da olabilir. Tailwind ile buna nadiren ihtiyaç duyarsınız, çünkü v4’te her data özniteliği için bir varyant vardır: data-highlighted:bg-accent, data-highlighted mevcutken uygulanır.
Dokümanlar, değişebilen üretilmiş ID’ler yerine bu öznitelikleri stillendirmenizi istiyor. Her bileşen sayfasında, her parçanın tam olarak neyi sunduğunu listeleyen bir Styling bölümü ve bir Data Attributes bölümü var. O liste sözleşmedir.
Tailwind v4’ü ve ui-sdk katmanını kurun
UI SDK’nın kendine ait biraz CSS’e ihtiyacı var, örneğin kapalı bir açılır öğeyi gizli tutmak için. Bu CSS’i runtime’da ui-sdk adlı bir cascade katmanında ekler. Sayfa runtime CSS’i nereye eklerse eklesin stilleriniz kazansın diye katmanı global stil dosyanızda, Tailwind import’unun üstünde en başta tanımlayın:
Token listesinin tamamı, aynı adları geçersiz kılan bir .dark bloğuyla birlikte shadcn/ui’ın ürettiği listedir. Burada kısalttım. UI SDK’daki hiçbir şey bu token’ları okumaz. Yalnızca aşağıdaki sınıflar onları kullandığı için önemlidirler; bu da bir UI SDK bileşeniyle bir shadcn/ui bileşeninin tek bir temayı paylaşabileceği anlamına gelir.
Bağlanacak bir provider ya da paketten import edilecek bir stil dosyası yok. Stiller için 'unsafe-inline' olmadan bir Content Security Policy kullanıyorsanız Styling and Animation kılavuzunun CSP bölümünü okuyun, çünkü runtime CSS bir nonce ya da oluşturulmuş stil sayfaları gerektirir.
Select: durum öznitelikleri ve animasyonlu bir liste
Bir Select’in etiketi, tetikleyicisi, değeri, içerik açılır öğesi ve öğeleri vardır. Bu örnek, seçili öğede onay işareti olan ve liste açılıp kapanırken kısa bir ölçeklenme ve solma animasyonu yapan bir rol seçici gösteriyor:

Her sınıfın okuduğu şey:
data-open:bg-accent, liste açıkken tetikleyiciyi renkli tutar.data-placeholder:hiçbir şey seçili değilken tetikleyiciyi stillendirir. Varsayılan bir değer olduğu için burada hiç uygulanmaz, ama tutmanın bir maliyeti yok.min-w-(--anchor-width)listeyi en az tetikleyici kadar geniş yapar. Tailwind v4, CSS değişkeni değerinivar()olmadan(--name)olarak yazar.origin-(--transform-origin)listeyi açıldığı taraftan ölçekler. UI SDK bu değişkeni içerik üzerinde ayarlar.data-highlighted:işaretçinin ya da klavye odağının altındaki öğeyi işaretler. Seçili öğelerdata-selectedalır veSelectItemIndicatoryalnızca seçili bir öğenin içinde render edilir.
items geçmek isteğe bağlı, ama dokümanlar bunu öneriyor: tetikleyici liste render edilmeden önce seçili etiketi gösterebilir ve ajan köprüsü liste kapalıyken seçenekleri okuyabilir.
Klavye vurgusunu işaretçi vurgusundan ayırmanız gerekirse, içerikte data-modality="keyboard" ya da "pointer" bulunur. Yalnızca klavye kullanıcılarına görünen bir odak halkası, öğede in-data-[modality=keyboard]:data-highlighted:ring-2 olur.
Açılır öğenin yaşam döngüsü
Yukarıdaki animasyon yalnızca bir geçiş sırasında var olan iki öznitelik kullanır. İçerik ilk karesinde data-starting-style alır, böylece geçişin başlayacağı bir durum olur. Kapanırken data-ending-style alır ve geçiş ya da animasyon bitene kadar sayfada kalır.
- KapalıSayfada değil
- İlk kare
data-opendata-starting-styleAnimasyonun başladığı durumu stillendirin - Açık
data-openNormal stillerine geçiş yapar - Kapanıyor
data-closeddata-ending-styleÇıkış bitene kadar sayfada kalır - Unmount edildiYa da keepMounted ile gizli
Bunun pratikte çalışmasını üç ayrıntı sağlıyor.
Birincisi, çıkış stillerini data-closed üzerine değil, data-ending-style üzerine koyun. data-closed durumu tanımlar, ama dokümanlar bunun öğeyi çıkış için sayfada tutmadığını açıkça söylüyor.
İkincisi, UI SDK açılır öğeleri tarayıcının top layer’ını kullanır. İçerik yerinde, bileşenin alt öğesi olarak render edilir ve tarayıcı onu sayfanın üstüne çıkarır. Deneme uygulamamda açık içerik popover="manual" taşıyordu ve :popover-open ile eşleşiyordu. Portal ya da Positioner parçası yoktur ve yapışkan bir başlığın üstünde durup durmayacağına z-index karar vermez.
Üçüncüsü, Motion gibi bir animasyon kütüphanesi kapalı içeriğin DOM’da kalmasını istiyorsa keepMounted ayarlayın. onOpenChangeComplete, açılma ya da kapanma animasyonu dahil bittikten sonra çalışır.
Popover: girişten daha hızlı bir çıkış
Giriş ve çıkışın aynı zamanlamaya ihtiyacı yok. 200ms’de kayarak giren ve 100ms’de solarak çıkan bir popover, iki yönde de 200ms süren birinden daha hızlı hissettirir. Çıkışın kendi özniteliği olduğu için Tailwind ona kendi süresini verebilir:

Giriş -translate-y-1, scale-95 ve opacity-0 değerlerinden durağan duruma geçer. Çıkış yalnızca data-ending-style:duration-100 ile ölçeklenir ve solar. motion-reduce:transition-none daha az hareket isteyenler için ikisini de kapatır; popover yine açılıp kapanır.
PopoverTitle ve PopoverDescription görünümün ötesinde önemlidir. İçerik bir dialog’dur ve bu parçalar ona erişilebilir bir ad ve açıklama verir. Başlık yoksa içeriğe bir aria-label verin.
Rating: yıldızları değil, dolguyu stillendirin
Rating, yıldız olarak çizilmiş bir radio grubudur. Tek bir RatingItem render edersiniz, Rating onu her yıldız için tekrarlar. Her öğe, gösterilen puanın kendi yıldızının ne kadarını kapladığını 0 ile 1 arasında --rating-fill ile bildirir. data-filled tam yıldızları, data-partial yarım puanın bittiği yıldızı, data-highlighted ise işaretçinin önizlediği puana kadarki yıldızları işaretler.
--rating-fill sayesinde yarım yıldızlar iki ikon yerine tek bir gradyandan çıkar:

step={0.5} ile her yarım yıldız, "3.5 stars" gibi adlandırılmış kendi radio’sudur. Radio’lar, Rating’in ikonunuzun üzerine satır içi stillerle yerleştirdiği düğmelerdir; öğe de aynı şekilde position: relative alır. Bu yüzden span ek bir sınıf olmadan absolute inset-0 kullanabilir.
Klavye, sizin hiçbir şey yapmanıza gerek kalmadan çalışır. Testimde işaretli yıldıza odaklanıp sağ ok tuşuna basınca puan 3.5’ten 4’e çıktı ve dolgu da onu izledi.
Maskeyi odaklanabilir öğeden uzak tutun
İlk sürümüm yıldız maskesini doğrudan RatingItem üzerine koyuyordu. Tab ile girene kadar doğru görünüyordu: CSS maskesi, çerçeveler dahil öğenin çizdiği her şeyi kırpar, bu yüzden odak halkası kayboldu. Maskeyi içteki bir span’e taşımak sorunu çözdü. Öğe yuvarlak çerçevesini has-focus-visible: ile korur, [&_button]:outline-none ise içerideki yarım genişlikteki radio düğmesinde tarayıcının varsayılan halkasını gizler; yoksa bu halka yarım yıldızın etrafına ince bir kutu çizer.

Bu UI SDK’ya özgü değil, ama ikon biçimli maskeler puanlamaları çizmenin yaygın bir yoludur; bu yüzden yayınlamadan önce klavyeyle kontrol etmeye değer.
Color Picker: çizimi CSS değişkenleri yapar
Color Picker, dördü arasında en çok değişkeni sunan bileşen. Her parça geçerli rengi --color-picker-color olarak yazar. Alan, iki kanalı için bir dizi gradyan olan --color-area-background değerini alır, her kaydırıcı da --color-slider-background değerini. Sınıflarınız parçalara bir boyut verir ve bu değişkenleri arka plan olarak kullanır.

Bunu kopyalamadan önce bilmeye değer şeyler:
- Popover, ColorPicker’ın içine girer. Böylece açılır öğedeki parçalar ve tetikleyicideki renk örneği aynı rengi düzenler ve gösterir.
bg-(image:--color-area-background), Tailwind v4’ün tür ipucudur.image:olmadan Tailwind değişkeni bir renk olarak okur ve gradyanlar boyanmaz.- Tutamaçlar
position: absoluteve yüzde cinsinden konumlarını satır içi stil olarak alır. Sınıfınızın yalnızca her tutamacı kendi noktasında ortalaması gerekir;-translate-1/2bunu yapar. Kaydırıcı tutamacı ayrıcatop-1/2ister, çünkü satır içi stili yalnızca iz boyunca konumu ayarlar. - Renk örneği ekran okuyucular için, rengin adını taşıyan bir görseldir. Benim yapımda
#6366f1için "vivid purple" olarak okundu. Tetikleyiciyi kendiniz adlandırın, çünkü görünen metin tek başına rengin ne için olduğunu söylemeyebilir. - OKLCH için alanın içine
ColorPickerAreaCanvasekleyin. Bir gradyan OKLCH’nin gamut sınırını çizemez, bu yüzden canvas onu piksel piksel boyar.
Düz CSS’te data-slot ile stil vermek
Her parçaya yardımcı sınıflar vermek bir seçenek. Diğeri data-slot’a göre yazılmış bir stil dosyası; bu, markup’ı kısa tutar ve bir bileşenin görünümünü tek bir yerde toplar:
UI SDK dokümanları kendi demolarını da düz CSS ile stillendiriyor. Bu işe yarıyor çünkü data-slot, bir parçanın her zaman koruduğu özniteliklerden biri: dokümanlar onu rollerin ve tabindex’in yanında, prop’larınızın geçersiz kılamayacağı yönetilen öznitelikler arasında listeliyor. Her bileşen dosyasının parçaları sarıp kendi sınıflarını eklediği shadcn/ui tarzı bir projede parçalardaki yardımcı sınıflar genellikle daha basittir. Düz CSS teması ise toptan değiştirmeye daha uygundur.
shadcn tarzı sınıfları Radix’ten ya da Base UI’dan taşımak
Bileşenleriniz shadcn/ui’dan geliyorsa sınıflarınızın çoğu aynen taşınır. Farklar birkaç öznitelik adında ve açılır öğe yapısındadır.
| Konu | Radix | Base UI | UI SDK |
|---|---|---|---|
| Açık durum | data-[state=open]: | data-open: | data-open: |
| Giriş stilleri | data-[state=open] üzerinde keyframe’ler | data-starting-style: | data-starting-style: |
| Çıkış stilleri | data-[state=closed] üzerinde keyframe’ler | data-ending-style: | data-ending-style: |
| Dönüşüm orijini | --radix-popover-content-transform-origin | --transform-origin | --transform-origin |
| Tetikleyici genişliği | --radix-popover-trigger-width | --anchor-width | --anchor-width |
| Açılır öğe parçaları | Portal, Content | Portal, Positioner, Popup | Yalnızca Content |
Base UI’dan geliyorsanız açılır öğelerin öznitelik adları aynıdır, bu yüzden sınıf string’leri çoğunlukla olduğu gibi taşınır. Portal ve Positioner sarmalayıcılarını silip side, align ve sideOffset gibi konumlandırma prop’larını içeriğe taşırsınız. Radix’ten geliyorsanız data-[state=…] varyantlarını ve keyframe animasyonlarını başlangıç ve bitiş öznitelikleriyle değiştirirsiniz. Her iki durumda da her bileşenin Data Attributes listesini kontrol edin, çünkü ortak görünen adlar farklı olabilir. Örneğin Base UI’ın Select’inin kendi öznitelik seti vardır, UI SDK’nınkinin de.
Üç kütüphanenin stilin ötesinde bütün olarak karşılaştırması UI SDK, Base UI ve Radix yazısında.
Beni zorlayanlar
Tekrarlamamanız için kısa bir liste:
- Çıkış için
data-closedstillendirmek. Hiçbir şey animasyonlanmaz.data-ending-stylekullanın. @layer ui-sdk;satırını unutmak. En başta olmazsa runtime CSS, cascade’de stillerinizden sonra gelebilir.- Odaklanabilir parçalarda maskeler. Odak çerçevesini kırparlar. İçteki bir öğeyi maskeleyin.
--available-heightdeğerini bir alt öğeden okumak. Dokümanlar bunun miras alınmadığını belirtiyor. Değeri açılır öğede kopyalayın, örneğin[--list-height:var(--available-height)], ve aşağıdamax-h-(--list-height)ile okuyun.- Değişkenlerden gradyanlar.
bg-(--name)değil,bg-(image:--name)kullanın.
UI SDK bir beta ve bu yazı yazılırken bir günlük. Buradaki her şey 0.1.0-beta.1’de dokümante edildiği gibi çalıştı ve node_modules/@ui-sdk/react/docs/ altındaki paketle gelen dokümanlardaki öznitelik listeleri, bileşenlerin render ettikleriyle eşleşiyor. HextaUI, bileşenlerinin Base UI sürümünden ayrı, stillendirilmiş bir UI SDK sürümü üzerinde çalışıyor; bu yazı da ilk parçaları yaparken öğrendiklerim.




