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?

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:

typescript
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:

css
: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:

typescript
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: { ... }
  },
})
typescript
<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

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:

typescript
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:

typescript
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:

typescript
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 }
typescript
<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.

typescript
<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

Eski bir bileşen dosyasını düzenliyorsanız: forwardRef: React.forwardRef<...> yerine React.ComponentProps<...> türünü kullanın., Bileşenden ref={ref} satırını kaldırın., Stil...

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<...> yerine React.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 displayName satı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.

Varyantları paylaşmak: toggleVariants ve buttonVariants

cva fonksiyonları dışa aktarıldığı için bir bileşenin görünümü başka bir bileşende tekrar kullanılabilir. toggle-group.tsx, toggle.tsx dosyasındaki toggleVariants fonksiyonunu içe aktarır ve gruptaki her öğeye uygular; grubun variant ve size değerleri bir React context ile öğelere taşınır. Bu yüzden toggle.tsx dosyasına eklediğiniz yeni bir boyut, ToggleGroup için de geçerli olur. Aynı şekilde alert-dialog.tsx ve pagination.tsx dosyaları buttonVariants kullanır; butona eklediğiniz bir varyant, bu bileşenlerde de sınıf olarak çağrılabilir.

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):

bash
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.

bash
npx shadcn@latest add button --overwrite

Düzenlemek mi, sarmalamak mı?

YaklaşımNe zaman uygunDikkat edilecek
ui dosyasını düzenlemekDeğişiklik her kullanımda geçerli olacaksa (yeni varyant, yeni varsayılan)add --overwrite değişikliği siler
className ile tek seferlik ezmekYalnızca bir yerde farklı görünüm gerekiyorsaAynı sınıflar çok yerde tekrar ediyorsa varyanta taşıyın
Yeni bir sarmalayıcı bileşenDavranış 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. Button parametredeki 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-warning gibi bir sınıf, değişken @theme inline ile 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-slot paketi kurulu olmalı ve Slot import 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ı