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?

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:
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:
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.
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:
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:
<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:
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?

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çindecard-actionparçası varsa ızgara düzenini iki sütuna çevirir (has-data-[slot=card-action]:grid-cols-[1fr_auto]).alert.tsx:destructivevaryantı, alt öğelerdenalert-descriptionolanı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
FieldGroupbileşeninedata-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?

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ça | Dosya | Görevi |
|---|---|---|
| cn | lib/utils.ts | Sınıfları birleştirir ve çakışmaları ayıklar |
| clsx | npm paketi | Koşullu ve karışık sınıf değerlerini tek dizgeye çevirir |
| tailwind-merge (twMerge) | npm paketi | Aynı özelliği hedefleyen Tailwind sınıflarından sondakini bırakır |
| aliases.utils | components.json | CLI'nin cn için yazacağı içe aktarma yolu |
| data-slot | components/ui/*.tsx | Parç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
uidosyası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