shadcn/ui bileşenlerini özelleştirmenin resmî yolu, projenize kopyalanan components/ui/*.tsx dosyasını doğrudan düzenlemektir. Butona yeni bir renk ya da boyut eklemek için o dosyadaki cva() çağrısının variants nesnesine bir anahtar eklersiniz; TypeScript türleri VariantProps sayesinde kendiliğinden güncellenir. asChild desteği isteyen kendi bileşenleriniz için de aynı dosyalarda kullanılan Slot kalıbını kopyalayabilirsiniz.
Bu yazı, resmî belgelerin "kodu açık bırakma" yaklaşımını depodaki button.tsx, toggle.tsx, toggle-group.tsx ve button-group.tsx kaynaklarıyla birlikte inceliyor: cva'nın yapısı, yeni varyant ve boyut ekleme, varsayılanı değiştirirken düşülen tuzak, data-slot ile dışarıdan stil verme ve CLI'ın üzerine yazma davranışı. Butonun hazır varyantları Button varyantları yazısında, sınıf birleştirme mantığı cn() fonksiyonu yazısında. Tüm seri: shadcn/ui öğrenme rehberi.
Neden bileşen dosyasını düzenliyoruz?

Belgelerin giriş sayfası, shadcn/ui'ın bir bileşen kütüphanesi olmadığını, kendi bileşen kütüphanenizi kurmanın bir yolu olduğunu söyler. Geleneksel kütüphanelerde bir butonun davranışını değiştirmek için stilleri ezersiniz ya da bileşeni sarmalarsınız; shadcn/ui'da ise belgelerin ifadesiyle butonun kodunu doğrudan düzenlersiniz. Bu ilkeye "Open Code" adı verilir: bileşenin en üst katmanı, yani tasarım sisteminize en yakın kısmı, değişikliğe açıktır.
Aynı sayfadaki sık sorulan soru, güncellemelerin nasıl geleceğini de açıklar: davranışın çekirdeği radix-ui ya da input-otp gibi bağımlılıklarda durur ve bu paketleri güncelleyerek düzeltmeleri alırsınız. Sizin düzenlediğiniz stil katmanı ise kütüphaneye bağlı değildir. Bu ayrım, özelleştirmeyi korkmadan yapabilmenizin temelidir: bir butonun rengini değiştirmek Radix'in klavye ya da odak davranışına dokunmaz.
cva() nasıl çalışır?
class-variance-authority (kısaca cva), shadcn/ui'ın manuel kurulum sayfasında listelenen temel bağımlılıklardan biridir. cva() iki argüman alır: her durumda uygulanacak temel sınıf dizisi ve bir ayar nesnesi. Ayar nesnesindeki variants, her özellik adını (örneğin variant, size) bir değer tablosuna bağlar; defaultVariants ise değer verilmediğinde hangisinin seçileceğini söyler. Dönen fonksiyonu çağırdığınızda temel sınıflar ile seçilen değerlerin sınıflarından oluşan tek bir metin elde edersiniz.
Aşağıda components/ui/toggle.tsx dosyasındaki tanımın kısaltılmış hâli var. Yapı butondakiyle aynıdır, yalnızca daha kısa olduğu için okuması kolaydır:
import { cva, type VariantProps } from "class-variance-authority"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium ...",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)VariantProps<typeof toggleVariants> türü bu nesneden variant ve size özelliklerinin türlerini çıkarır. Bileşenin props türü bu türle birleştirildiği için yeni bir anahtar eklediğiniz anda editörünüz onu önerir, olmayan bir değeri yazdığınızda da hata verir. Türlerin TypeScript tarafındaki işleyişi bu yazının konusu değil; merak ederseniz TypeScript öğrenme rehberine bakabilirsiniz.
className nereye gidiyor?
Kaynaklarda iki farklı yazım göreceksiniz ve ikisi de aynı sonuca varır. button.tsx ve toggle.tsx sınıfı cva'nın içine verir: cn(buttonVariants({ variant, size, className })). cva, kendisine verilen className değerini ürettiği metnin sonuna ekler. button-group.tsx ise dışarıda birleştirir: cn(buttonGroupVariants({ orientation }), className). Her iki durumda da en son cn() çalışır ve tailwind-merge çakışan sınıflardan sonuncuyu tutar; yani kullanıcının verdiği sınıf varsayılanı ezer.
Bu sıralama önemlidir. Kendi bileşeninizi yazarken className değerini cn() çağrısının en sonuna koyun; başa koyarsanız varsayılan sınıflar sizinkini ezer ve "className çalışmıyor" sorunuyla karşılaşırsınız.
Yeni bir renk varyantı eklemek
Diyelim ki uyarı amaçlı sarı bir buton istiyorsunuz. Önce rengin temada var olması gerekir. Tema belgesindeki "Adding new colors" bölümü, :root ve .dark altında değişkeni tanımlayıp @theme inline ile Tailwind'e tanıtmayı gösterir. Belgedeki örnek tam olarak bir warning rengidir; app/globals.css dosyasına şunu eklersiniz:
:root {
--warning: oklch(0.84 0.16 84);
--warning-foreground: oklch(0.28 0.07 46);
}
.dark {
--warning: oklch(0.41 0.11 46);
--warning-foreground: oklch(0.99 0.02 95);
}
@theme inline {
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}Artık bg-warning ve text-warning-foreground sınıfları kullanılabilir. İkinci adımda components/ui/button.tsx dosyasında variant tablosuna yeni anahtarı eklersiniz. Aşağıdaki satır bizim uyarlamamızdır; diğer dolu varyantların kalıbını (bg-..., text-...-foreground, hover:bg-.../90) izler:
const buttonVariants = cva("...", {
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
// ... mevcut varyantlar
warning: "bg-warning text-warning-foreground hover:bg-warning/90",
},
// size: { ... }
},
})<Button variant="warning">Değişiklikleri geri al</Button>Renk değişkenleri ve oklch biçimi hakkında daha fazlası tema ve CSS değişkenleri yazısında; CSS özel özelliklerinin kendisi ise CSS değişkenleri yazısında anlatılıyor.
Yeni bir boyut eklemek

Boyut eklemenin resmî bir örneği zaten var: Button sayfasının değişiklik günlüğü, 24 Eylül 2025'te icon-sm ve icon-lg boyutlarının nasıl ekleneceğini size nesnesine iki satır yazarak gösterir. Kendi boyutunuzu da aynı yere eklersiniz. Aşağıdaki xl satırı bizim örneğimizdir; mevcut lg satırının kalıbını büyütür:
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
xl: "h-12 rounded-md px-8 has-[>svg]:px-6",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},Anahtar adında tire varsa ("icon-sm" gibi) tırnak içinde yazılması gerektiğine dikkat edin. Yeni boyut, ButtonGroup gibi butonu saran bileşenlerde de çalışır, çünkü onlar boyutu her butona ayrı ayrı verdiğiniz size özelliğinden alır.
Varsayılanı değiştirirken düşülen tuzak
Butonların varsayılan olarak outline görünmesini istediğinizi düşünün. İlk akla gelen, defaultVariants içinde variant: "outline" yazmaktır. Ancak button.tsx kaynağına bakarsanız fonksiyonun parametrelerinde de varsayılan değer olduğunu görürsünüz:
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
// ...
className={cn(buttonVariants({ variant, size, className }))}
}Siz variant vermediğinizde parametre "default" olur ve bu değer cva'ya açıkça iletilir. cva yalnızca değer verilmediğinde defaultVariants tablosuna baktığı için sizin değişikliğiniz hiç devreye girmez. Bu nedenle varsayılanı değiştirirken iki yeri birlikte güncelleyin: fonksiyon parametresindeki variant = "default" ifadesini ve defaultVariants nesnesini. Parametredeki değer aynı zamanda data-variant özniteliğine de yazıldığı için tutarlılık açısından da bu gerekir. toggle.tsx ise parametrede varsayılan kullanmaz; orada yalnızca defaultVariants yeterlidir. Hangi kalıbın kullanıldığını anlamak için düzenlediğiniz dosyayı okumak her zaman en güvenilir yoldur.
Slot ve asChild kalıbını kendi bileşeninize eklemek
asChild, shadcn/ui bileşenlerinde tekrar eden bir kalıptır: Button, ButtonGroupText ve Item gibi bileşenler aynı satırı kullanır. Kalıp üç parçadan oluşur: props türüne asChild?: boolean eklemek, @radix-ui/react-slot paketinden Slot içe aktarmak ve const Comp = asChild ? Slot : "div" ile hangi öğenin üretileceğini seçmek. Aşağıdaki Etiket bileşeni bizim örneğimizdir ve ButtonGroupText kaynağındaki yapıyı birebir izler:
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
function Etiket({
className,
asChild = false,
...props
}: React.ComponentProps<"span"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="etiket"
className={cn(
"bg-muted inline-flex items-center rounded-md px-2 text-sm",
className
)}
{...props}
/>
)
}
export { Etiket }<Etiket>Taslak</Etiket>
<Etiket asChild>
<a href="/yazilar/taslaklar">Taslaklar</a>
</Etiket>İkinci kullanımda ekranda bir span değil, etiket stiline sahip bir bağlantı oluşur. Slot, sınıfları ve diğer özellikleri tek çocuk öğeye taşıdığı için asChild verildiğinde içeriye tek bir öğe koymalısınız.
data-slot ile bileşenleri dışarıdan hedeflemek
Tailwind v4 sayfası, bileşenlerden forwardRef kaldırılırken her ilkel bileşene bir data-slot özniteliği eklendiğini ve bunun Tailwind ile stil verirken işe yarayacağını söyler. Bu öznitelik sayesinde bir bileşenin dosyasını açmadan, onu kapsayan öğe üzerinden stil verebilirsiniz. Depodaki kullanım örnekleri bunu iyi gösterir: button-group.tsx, içindeki select tetikleyicisini [&>[data-slot=select-trigger]] seçicisiyle hedefler; dashboard bloğundaki grafik kartı ise *:data-[slot=toggle-group-item]:!px-4 sınıfıyla tüm toggle öğelerinin dolgusunu büyütür.
<ToggleGroup
type="single"
value={timeRange}
onValueChange={setTimeRange}
variant="outline"
className="*:data-[slot=toggle-group-item]:!px-4"
>
<ToggleGroupItem value="90d">Son 3 ay</ToggleGroupItem>
<ToggleGroupItem value="30d">Son 30 gün</ToggleGroupItem>
<ToggleGroupItem value="7d">Son 7 gün</ToggleGroupItem>
</ToggleGroup>Kendi bileşeninize de data-slot eklemeniz, onu başkalarının (ve sizin) dışarıdan hedefleyebilmesini sağlar. Yukarıdaki Etiket örneğinde data-slot="etiket" bu yüzden var. Butonun data-variant ve data-size öznitelikleri de aynı amaca hizmet eder.
Eski bir bileşen dosyasını düzenliyorsanız: forwardRef

Tailwind v3 ve React 18 döneminde eklenmiş bir bileşen dosyasını açtığınızda yukarıdaki örneklerden farklı bir yapı görebilirsiniz: bileşen React.forwardRef ile sarılmıştır, sonunda bir displayName satırı vardır ve data-slot özniteliği yoktur. Tailwind v4 sayfası, bileşenlerin güncel sürümünde forwardRef çağrılarının kaldırıldığını ve türlerin düzenlendiğini söyler; bu değişikliğin geriye dönük olarak zorunlu olmadığını da belirtir. Eski dosyanızı yeni kalıba taşımak isterseniz sayfa, React'in remove-forward-ref codemod'unu ya da şu elle adımları önerir:
React.forwardRef<...>yerineReact.ComponentProps<...>türünü kullanın.- Bileşenden
ref={ref}satırını kaldırın. - Stil verirken işe yarayacak bir
data-slotözniteliği ekleyin. - İsterseniz bileşeni adlandırılmış bir fonksiyona çevirip
displayNamesatırını silin.
Varyant eklemek için bu dönüşümü yapmanız gerekmez; cva tablosu iki yapıda da aynı yerde durur. Ancak bir dosyada hem eski hem yeni kalıbı karıştırmamak okunabilirliği korur. Geçişin diğer adımları Tailwind v4 geçişi yazısında anlatılıyor.
CLI tekrar eklerse ne olur?
Bileşen dosyasını düzenledikten sonra aynı bileşeni add ile yeniden eklemeye çalışırsanız CLI dosyanın zaten var olduğunu görür. Kaynak koddaki davranış şöyledir: dosyanın içeriği yeni sürümle aynıysa dosya atlanır; farklıysa bir onay sorusu gösterilir ve varsayılan cevap "hayır"dır. Sorunun metni CLI kaynağındaki update-files.ts dosyasında şöyle geçer (dosya adı yerine ilgili dosya yazılır):
The file button.tsx already exists. Would you like to overwrite?-o ya da --overwrite seçeneği bu soruyu sormadan üzerine yazar; CLI belgesindeki add seçenekleri arasında "overwrite existing files" olarak listelenir. Düzenlediğiniz bir dosyada bu seçeneği kullanırsanız değişiklikleriniz kaybolur. Bu yüzden özelleştirdiğiniz dosyaları sürüm kontrolünde tutun ve güncellemeleri bileşen sayfalarının "Changelog" bölümündeki elle yapılacak adımlarla uygulayın (butondaki icon-sm notu gibi). CLI komutlarının tamamı CLI komutları yazısında.
npx shadcn@latest add button --overwriteDüzenlemek mi, sarmalamak mı?
| Yaklaşım | Ne zaman uygun | Dikkat edilecek |
|---|---|---|
| ui dosyasını düzenlemek | Değişiklik her kullanımda geçerli olacaksa (yeni varyant, yeni varsayılan) | add --overwrite değişikliği siler |
| className ile tek seferlik ezmek | Yalnızca bir yerde farklı görünüm gerekiyorsa | Aynı sınıflar çok yerde tekrar ediyorsa varyanta taşıyın |
| Yeni bir sarmalayıcı bileşen | Davranış ekliyorsanız (ör. onay isteyen buton) | Sarmalayıcı da className ve diğer props değerlerini geçirmeli |
Sık yapılan hatalar
- Yalnızca defaultVariants'ı değiştirmek.
Buttonparametredeki varsayılanı cva'ya açıkça ilettiği için değişiklik etkisiz kalır; iki yeri birlikte güncelleyin. - Temada olmayan bir renk sınıfı yazmak.
bg-warninggibi bir sınıf, değişken@theme inlineile tanıtılmadan üretilmez ve buton renksiz kalır. - className'i cn() çağrısının başına koymak. tailwind-merge sondakini tuttuğu için kullanıcının sınıfı kaybolur.
- Tireli anahtarı tırnaksız yazmak.
icon-sm: "size-8"geçerli bir JavaScript nesne anahtarı değildir;"icon-sm"yazın. - Düzenlenmiş dosyada --overwrite kullanmak. Özelleştirmeleriniz yeni sürümle değişir; önce farkı sürüm kontrolünde inceleyin.
- Kendi bileşeninde asChild kullanıp Slot'u içe aktarmamak.
@radix-ui/react-slotpaketi kurulu olmalı veSlotimport edilmeli.
Sık Sorulan Sorular
cva yerine sadece className kullansam olmaz mı?
Tek bir yer için olur. Ancak aynı görünüm birçok yerde gerekiyorsa cva tablosuna eklemek, değeri tek bir isimle (variant="warning") çağırmanızı ve TypeScript'in yazım hatalarını yakalamasını sağlar.
Bileşenleri düzenlersem shadcn/ui güncellemelerini nasıl alırım?
Davranış düzeltmeleri çoğunlukla Radix gibi alt bağımlılıklardan gelir ve paket güncellemesiyle alınır. Stil katmanındaki değişiklikler için bileşen sayfalarının değişiklik günlüğündeki adımları elle uygularsınız. Sürüm geçmişi yenilikler ve sürüm geçmişi yazısında.
compoundVariants kullanabilir miyim?
Bu bir cva özelliğidir, shadcn/ui belgeleri ve new-york-v4 bileşenleri onu kullanmaz. Gerekirse cva'nın kendi belgesine bakmanız gerekir; bu yazı yalnızca shadcn/ui kaynaklarında görülen kalıpları anlatıyor.
Tailwind önek (prefix) kullanıyorum, cva bozulur mu?
Değişiklik günlüğündeki Aralık 2023 notuna göre CLI, özel bir Tailwind önekiyle bileşen eklerken sınıflara öneki kendisi ekler ve bu cn, cva ve CSS değişkenleriyle birlikte çalışır. Elle eklediğiniz yeni varyant sınıflarına öneki sizin yazmanız gerekir.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: shadcn/ui öğrenme rehberi · Button varyantları · cn() fonksiyonu · Tema ve CSS değişkenleri · CLI komutları