cn(), shadcn/ui projelerinde lib/utils.ts dosyasında duran küçük bir yardımcı fonksiyondur: aldığı sınıf değerlerini önce clsx ile tek bir sınıf dizgesine dönüştürür, sonra tailwind-merge paketinin twMerge fonksiyonuyla çakışan Tailwind sınıflarını ayıklar. Her bileşen kendi varsayılan sınıflarını ve sizin verdiğiniz className değerini bu fonksiyondan geçirir; böylece bileşeni kullanırken verdiğiniz sınıf, varsayılanla çakıştığında sizinki geçerli olur.

Bu yazıda fonksiyonun kendisini, iki kütüphanenin neden birlikte kullanıldığını, bileşen kaynaklarındaki tipik kullanım kalıplarını, className ile stili ezmenin doğru yolunu ve Tailwind v4 güncellemesiyle her bileşene eklenen data-slot özniteliğini anlatıyoruz. Varyant tanımlarının kendisi (class-variance-authority) ayrı bir yazıda: cva ile bileşen özelleştirme.

cn fonksiyonu nerede ve nasıl görünür?

cn fonksiyonu nerede ve nasıl görünür?

CLI belgesine göre init komutu bağımlılıkları kurarken cn yardımcısını da projenize ekler. Elle kurulum yapıyorsanız manuel kurulum sayfası önce class-variance-authority, clsx, tailwind-merge, lucide-react ve tw-animate-css paketlerini kurmanızı, ardından lib/utils.ts dosyasını oluşturmanızı ister. Dosyanın tamamı şu birkaç satırdır:

typescript
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Fonksiyon rest parametresiyle istediğiniz kadar argüman alır. Tür olarak clsx paketinin dışa aktardığı ClassValue kullanılır; bu, clsx'in kabul ettiği değer biçimlerinin tamamını kapsar. Gövde tek satırdır: argüman dizisi clsx'e, clsx'in ürettiği dizge twMerge'e gider. Depodaki registry/new-york-v4/lib/utils.ts dosyası da birebir bu içeriğe sahiptir; yani resmi bileşenlerin beklediği cn budur.

Bileşenler bu fonksiyonu @/lib/utils yolundan içe aktarır. Bu yol sabit değildir: components.json içindeki aliases.utils alanı CLI'ye yardımcının nerede olduğunu söyler ve CLI eklediği bileşenlerin içe aktarmasını buna göre yazar. Monorepo şablonunda bu değer @workspace/ui/lib/utils olur. Takma adların TypeScript tarafındaki karşılığı için TypeScript paths ve alias ayarları yazısına bakın.

clsx ve tailwind-merge neden birlikte?

İki paket farklı sorunları çözer. clsx sınıfları birleştirme işini üstlenir: düz metinleri, koşula bağlı değerleri ve diziler gibi biçimleri alıp aralarında boşluk olan tek bir sınıf dizgesi üretir, yanlış ya da boş değerleri atlar. Bu sayede koşullu sınıfları üçlü ifadeler veya mantıksal operatörlerle doğrudan argüman olarak verebilirsiniz.

Ancak clsx Tailwind'i tanımaz. Bir bileşenin varsayılan sınıflarında rounded-xl varken siz rounded-none verirseniz clsx ikisini de dizgeye koyar ve hangisinin kazanacağını CSS dosyasındaki kural sırası belirler, sınıf dizgesindeki sıra değil. tailwind-merge burada devreye girer: aynı CSS özelliğini hedefleyen Tailwind sınıflarından yalnızca sonda geleni bırakır. Sonuç olarak "sonra yazılan kazanır" kuralı sınıf dizgesi düzeyinde öngörülebilir hale gelir. shadcn/ui'nin bileşenleri kolayca ezilebilir varsayılanlarla sunabilmesi bu davranışa dayanır.

Tailwind v4 geçiş sayfası bu ilişkinin pratik bir sonucunu da not eder: Tailwind v3.4 ile gelen size-* sınıfı artık tailwind-merge tarafından tam desteklenir, bu yüzden w-4 h-4 yerine size-4 yazabilirsiniz. Aynı sayfadaki bağımlılık güncelleme komutu da tailwind-merge ve clsx paketlerini en son sürüme çeker. Tailwind sınıflarının kendisini öğrenmek için Tailwind CSS rehberine bakabilirsiniz.

Bileşen kaynaklarındaki cn kalıbı

components/ui klasöründeki dosyalara baktığınızda neredeyse her bileşenin aynı kalıbı izlediğini görürsünüz. card.tsx dosyasından kısaltılmış Card fonksiyonu bunun en sade örneğidir:

typescript
function Card({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card"
      className={cn(
        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
        className
      )}
      {...props}
    />
  )
}

Burada üç şey dikkat çeker. Birincisi, bileşen props türünü React.ComponentProps<"div"> ile alır; yani bir div öğesinin kabul ettiği her özelliği kabul eder. İkincisi, className ayrı çekilir ve cn çağrısında varsayılan sınıfların arkasına konur. Üçüncüsü, kalan props {...props} ile alttaki öğeye aktarılır. Sıra önemlidir: sizin sınıfınız sonda olduğu için tailwind-merge çakışmada onu tutar.

Varyantlı bileşenlerde kalıp biraz değişir ama mantık aynıdır. button.tsx içinde className, cva fonksiyonuna diğer varyant değerleriyle birlikte verilir ve sonuç cn'den geçer: cn(buttonVariants({ variant, size, className })). badge.tsx ve button-group.tsx ise className değerini cva dışında, ikinci argüman olarak verir: cn(badgeVariants({ variant }), className). Her iki yazımda da sizin sınıfınız en sona eklendiği için sonuç aynıdır.

typescript
const Comp = asChild ? Slot : "button"

return (
  <Comp
    data-slot="button"
    data-variant={variant}
    data-size={size}
    className={cn(buttonVariants({ variant, size, className }))}
    {...props}
  />
)

className ile stili ezmek

Bu kalıp sayesinde bir bileşenin görünümünü dosyasını açmadan, kullandığınız yerde değiştirebilirsiniz. Örneğin varsayılan Card yuvarlatılmış köşe ve gölgeyle gelir. Belirli bir sayfada düz köşeli ve gölgesiz bir kart istiyorsanız:

typescript
import { Card } from "@/components/ui/card"

export function DuzKart() {
  return (
    <Card className="rounded-none shadow-none">
      Duyurular
    </Card>
  )
}

cn çağrısı varsayılan dizgedeki rounded-xl ve shadow-sm sınıflarını sizin rounded-none ve shadow-none değerlerinizle değiştirir; diğer varsayılanlar (flex, gap-6, border gibi) olduğu gibi kalır. Değişiklik yalnızca bu kullanım için geçerlidir. Değişikliğin projedeki tüm kartlara yansımasını istiyorsanız doğru yer components/ui/card.tsx dosyasının kendisidir; shadcn/ui'nin açık kod yaklaşımı tam da bunu önerir. Renkleri tek tek sınıflarla ezmek yerine tema değişkenlerini değiştirmek ise genellikle daha temizdir; bkz. shadcn tema ve CSS değişkenleri.

Koşullu sınıflar

clsx sayesinde cn içine koşullu ifadeler de yazılabilir. Combobox belgesindeki örnek, seçili öğenin yanındaki onay ikonunu yalnızca o öğe seçiliyken görünür yapar:

typescript
<CheckIcon
  className={cn(
    "mr-2 h-4 w-4",
    value === framework.value ? "opacity-100" : "opacity-0"
  )}
/>

Aynı yaklaşımı kendi bileşenlerinizde de kullanabilirsiniz. Önemli olan, koşuldan bağımsız sınıfları ilk argümana, duruma bağlı olanları sonraki argümanlara koymak ve dışarıdan gelen className değerini her zaman en sona bırakmaktır. Böylece bileşeni kullanan kişi gerektiğinde koşullu sınıfı da ezebilir. Bu örneğin tamamı shadcn combobox yazısında.

Kendi bileşeninizi aynı kalıpla yazmak

shadcn/ui'deki bileşenlerle uyumlu küçük bir bileşen yazarken aynı yapıyı izlemek, onu da diğerleri gibi ezilebilir ve tahmin edilebilir yapar. Aşağıdaki components/durum-etiketi.tsx örneği card.tsx kalıbından uyarlanmıştır; aktif olup olmamasına göre renk değiştirir:

typescript
import * as React from "react"

import { cn } from "@/lib/utils"

function DurumEtiketi({
  className,
  aktif = false,
  ...props
}: React.ComponentProps<"span"> & { aktif?: boolean }) {
  return (
    <span
      data-slot="durum-etiketi"
      className={cn(
        "rounded-md border px-2 py-0.5 text-xs font-medium",
        aktif ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

export { DurumEtiketi }

Bu bileşen bg-primary, text-muted-foreground gibi tema sınıflarını kullandığı için açık ve koyu temada otomatik uyum sağlar. data-slot değeri, başka bir bileşenin içinde bu etiketi hedeflemek istediğinizde işinize yarar. Birden çok görünüm seçeneği (boyut, renk türü) gerekiyorsa koşullu ifadeler yerine cva ile varyant tanımlamak daha düzenli olur.

data-slot özniteliği ne işe yarar?

data-slot özniteliği ne işe yarar? card.tsx, alert.tsx, Calendar belgesi, Input Group belgesi

Tailwind v4 güncellemesinin değişiklik listesinde her ilkel parçanın artık stil için bir data-slot özniteliği taşıdığı yazar. Gerçekten de güncel kaynakta her parça kendi adını taşır: card, card-header, card-title, card-action, button, alert-description gibi. Düğme ek olarak data-variant ve data-size özniteliklerini de yazar. Bu öznitelikler, bir bileşenin içindeki ya da çevresindeki parçaları sınıf adlarına bağımlı olmadan seçmeyi sağlar.

Kaynak kodda ve belgelerde bunun birkaç tipik kullanımı vardır:

  • card.tsx: başlık bölümü, içinde card-action parçası varsa ızgara düzenini iki sütuna çevirir (has-data-[slot=card-action]:grid-cols-[1fr_auto]).
  • alert.tsx: destructive varyantı, alt öğelerden alert-description olanın metin rengini ayarlar (*:data-[slot=alert-description]:text-destructive/90).
  • Calendar belgesi: takvim bir kartın ya da popover'ın içindeyse arka planını şeffaf yapar ([[data-slot=card-content]_&]:bg-transparent).
  • Input Group belgesi: kendi yazdığınız bir girdiye data-slot="input-group-control" eklerseniz grubun odak davranışı ona da uygulanır.
  • Form belgeleri: onay kutusu grubunda doğru aralık için FieldGroup bileşenine data-slot="checkbox-group" eklemeniz istenir.

Kendi bileşenlerinizi Tailwind v4 öncesi forwardRef yapısından günceliyorsanız, geçiş sayfası adımlardan biri olarak data-slot eklemeyi önerir. Adımların tamamı shadcn Tailwind v4 geçişi yazısında.

tailwind.prefix ile cn birlikte çalışır mı?

Bazı projelerde shadcn/ui sınıflarının var olan bir tasarım sistemiyle çakışmaması için Tailwind sınıflarına önek eklenir. components.json içindeki tailwind.prefix alanına örneğin tw- yazdığınızda CLI, eklediği bileşenlerin sınıflarını bu önekle yazar. Changelog'un Aralık 2023 notuna göre bu özellik cn, cva ve CSS değişkenleriyle birlikte çalışır. Önek yalnızca CLI'nin eklediği dosyalara uygulanır; kendi yazdığınız bileşenlerde öneki sizin kullanmanız gerekir.

className ile ezmek mi, dosyayı düzenlemek mi?

className ile ezmek mi, dosyayı düzenlemek mi? lib/utils.ts, clsx, tailwind-merge (twMerge), aliases.utils

shadcn/ui'nin giriş sayfası, geleneksel kütüphanelerde bir düğmenin davranışını değiştirmek için stilleri ezmek ya da bileşeni sarmalamak gerektiğini, shadcn/ui'de ise doğrudan düğmenin kodunu düzenleyebileceğinizi vurgular. cn iki yolu da mümkün kılar; hangisini seçeceğiniz değişikliğin kapsamına bağlıdır. Tek bir sayfadaki istisna için className yeterlidir ve bileşen dosyasına dokunmadığınız için ileride add --overwrite ile güncellerken kaybedeceğiniz bir şey olmaz. Değişiklik tasarım sisteminizin parçasıysa, yani her kartın ya da her düğmenin öyle görünmesi gerekiyorsa, aynı sınıfı onlarca yerde tekrarlamak yerine components/ui içindeki dosyayı düzenlemek daha az hata üretir. Bu durumda dosya artık sizindir ve CLI ile üzerine yazarken değişikliklerinizi elle geri taşımanız gerekir. Yeni bir görünüm seçeneğiyse (örneğin ikinci bir düğme türü) en düzenli yol, dosyadaki cva tanımına yeni bir varyant eklemektir.

ParçaDosyaGörevi
cnlib/utils.tsSınıfları birleştirir ve çakışmaları ayıklar
clsxnpm paketiKoşullu ve karışık sınıf değerlerini tek dizgeye çevirir
tailwind-merge (twMerge)npm paketiAynı özelliği hedefleyen Tailwind sınıflarından sondakini bırakır
aliases.utilscomponents.jsonCLI'nin cn için yazacağı içe aktarma yolu
data-slotcomponents/ui/*.tsxParçaları stil için adıyla seçilebilir yapar

Sık yapılan hatalar

  • className'i başa koymak: cn(className, "rounded-xl ...") yazarsanız varsayılanlar sizin sınıfınızı ezer. Dışarıdan gelen değer her zaman sonda olmalı.
  • cn yerine düz şablon dizgesi kullanmak: İki sınıfı yalnızca yan yana yazmak çakışmaları ayıklamaz; sonuç CSS kural sırasına kalır.
  • lib/utils.ts dosyasını taşıyıp aliases.utils'i güncellememek: CLI yeni eklediği bileşenlerde eski yolu yazar ve derleme hata verir.
  • Renkleri her yerde sınıfla ezmek: Tutarlı bir değişiklik için tema değişkenlerini ya da ilgili ui dosyasını düzenleyin.
  • w-* h-* çiftlerini korumak: Hata değildir ama Tailwind v4 geçiş sayfası bunların yerine size-* kullanmayı önerir.

Sık Sorulan Sorular

cn fonksiyonunu kendim mi yazmalıyım?

init komutu onu sizin için ekler. Elle kurulum yapıyorsanız manuel kurulum sayfasındaki birkaç satırlık dosyayı kopyalamanız yeterlidir; adımlar shadcn manuel kurulum yazısında.

Sadece clsx kullanmak yeterli olmaz mı?

Koşullu sınıflar için evet, ama bileşen varsayılanlarını className ile ezmek için tailwind-merge gerekir; aksi halde aynı özelliği hedefleyen iki sınıf birlikte kalır.

cn fonksiyonunun adını ya da yerini değiştirebilir miyim?

Yerini değiştirebilirsiniz; aliases.utils alanını yeni yola göre güncellemeniz gerekir. Var olan bileşenler cn adıyla içe aktarma yaptığı için fonksiyon adını değiştirmek tüm ui dosyalarını düzenlemeyi gerektirir.

TypeScript kullanmıyorsam cn yine gerekli mi?

Evet. JavaScript sayfasına göre components.json içinde tsx değerini false yaptığınızda CLI bileşenleri .jsx uzantısıyla ekler; bu bileşenler de sınıflarını aynı yardımcıdan geçirir. Bu durumda takma adlar tsconfig.json yerine jsconfig.json dosyasındaki paths ayarıyla çözülür ve aliases.utils yine @/lib/utils gibi bir yolu gösterir. Fonksiyonun mantığı değişmez; yalnızca tür açıklamaları olmadan yazılır.

data-slot yerine kendi sınıf adlarımı kullanabilir miyim?

Kullanabilirsiniz; kod sizin. Ancak resmi bileşenler birbirini data-slot üzerinden tanıdığı için bu öznitelikleri silmek, örneğin kart başlığı veya girdi grubu gibi parçaların otomatik düzenini bozabilir.

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 · cva ile varyant · Button varyantları · tema ve CSS değişkenleri · components.json · Tailwind CSS rehberi