Tailwind'de Ölçek, Bakım ve Sürümler: Component Pattern'leri, Class Sıralama, tailwind-merge ve v3'ten v4'e Migration
Çakışan iki utility'den class attribute'unda sonra yazılan değil, CSS'te sonra gelen kazanır; bu yüzden component'ler variant'ları tam adlı bir class map'le vermeli ve dışarıdan gelen class'ları tailwind-merge ile birleştirmelidir. bookstore-web'in butonunda bunu kuruyor, tailwind-merge'ün özel token tuzağını, Prettier ile sıralamayı, npx @tailwindcss/upgrade'in v3 sayfasında neyi çevirip neyi kaçırdığını Playwright ile ölçüyor ve v4.0–v4.3 sürüm haritasını çıkarıyoruz.
Sürüm notu: Bu yazıdaki örnekler Tailwind CSS 4.3.5 ve 3.4.19, @tailwindcss/upgrade 4.3.5, tailwind-merge 3.7, Prettier 3.10 ve prettier-plugin-tailwindcss 0.8.1, Angular 22.2 (TypeScript 6.0), Node.js 24 ve Playwright 1.65 (Chromium 158) ile denenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı), sepet sayfasında "Remove" butonunu ortak buton component'iyle çiziyor. Component'in kendi class'ları bg-brand-600 px-6 py-2. Sepet sayfası bu butonu kırmızı ve biraz daha dar istiyor ve çağıran taraf bg-red-600 px-4 ekliyor. Element'in son hâli:
<button id="a" class="bg-brand-600 px-6 py-2 bg-red-600 px-4">Remove</button>
Çağıranın class'ları sonda. Buton kırmızı mı, mavi mi? Yatay boşluğu 16px mi (px-4), 24px mi (px-6)?
Bu yazı serinin bakım yazısı: büyüyen bir projede component API'lerini variant'larla kurmak, çakışan class'ları birleştirmek, class sırasını araçla sabitlemek, v3'ten v4'e geçmek ve sürümleri takip etmek. Kurumsal Sipariş ekranları, yönetim paneli ve katalog aynı buton, kart ve rozetleri paylaşmaya başladığında bunlar teorik sorun olmaktan çıkıyor.
Kazanan Class Sırası Değil, CSS Sırası
Tahmindeki butonu ve aynı class'ları ters sırayla yazdığımız ikinci bir butonu ölçtük:
node scripts/06-conflict.mjs
class="bg-brand-600 px-6 py-2 bg-red-600 px-4" → bg oklch(0.577 0.245 27.325), padding-inline 24px, display inline-block
class="bg-red-600 px-4 bg-brand-600 px-6 py-2" → bg oklch(0.577 0.245 27.325), padding-inline 24px, display inline-block
class="grid flex" → bg rgba(0, 0, 0, 0), padding-inline 0px, display grid
İki buton da kırmızı ve ikisinde de yatay boşluk 24px. Renkte çağıran kazandı, boşlukta component; class attribute'undaki sıra hiçbir şeyi değiştirmedi. Cascade'de aynı specificity'deki iki kuraldan stylesheet'te sonra geleni kazanır ve sırayı Tailwind belirliyor:
.flex {
.grid {
.bg-brand-600 {
.bg-red-600 {
.px-4 {
.px-6 {
bg-red-600, bg-brand-600'den sonra geldiği için kazanıyor; px-6 de px-4'ten sonra. Tailwind dokümanı da aynı kuralı söylüyor ve class="grid flex" örneğiyle gösteriyor. Bu sıralama sayfadan sayfaya, hatta yeni bir class eklenince değişebilen bir iç ayrıntı; ona güvenerek override yazılmaz. Çözüm iki parçalı: component'in dışarıya class değil anlamlı seçenekler vermesi, dışarıdan class kabul ediyorsa da çakışanları birleştirmesi.
Variant'lı Component API'si: Küçük Bir Class Map
bookstore-web'in butonu iki seçenek alıyor: intent (primary, secondary, danger) ve size (sm, md). Her seçeneğin class'ları bir Record içinde, tam adlarıyla:
import { Component, computed, input } from '@angular/core';
import { extendTailwindMerge } from 'tailwind-merge';
export type ButtonIntent = 'primary' | 'secondary' | 'danger';
export type ButtonSize = 'sm' | 'md';
// Bookstore'un özel font boyutları: tailwind-merge onları renk sanmasın
const merge = extendTailwindMerge({ extend: { theme: { text: ['hero'] } } });
const BASE =
'inline-flex items-center justify-center rounded-md font-medium focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50';
// her variant için bir kayıt, her class tam adıyla
const INTENT: Record<ButtonIntent, string> = {
primary: 'bg-brand-600 text-white hover:bg-brand-700 focus-visible:outline-brand-600',
secondary:
'border border-slate-300 bg-white text-ink-900 hover:bg-slate-50 focus-visible:outline-slate-400',
danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600',
};
const SIZE: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
};
export function buttonClasses(
intent: ButtonIntent = 'primary',
size: ButtonSize = 'md',
extra = '',
): string {
return merge(BASE, INTENT[intent], SIZE[size], extra);
}
@Component({
selector: 'button[appButton]',
template: '<ng-content />',
host: { '[class]': 'classes()' },
})
export class Button {
readonly intent = input<ButtonIntent>('primary');
readonly size = input<ButtonSize>('md');
// çağıranın eklediği class'lar; variant'ın class'larına karşı kazanır
readonly extra = input('', { alias: 'class' });
protected readonly classes = computed(() =>
buttonClasses(this.intent(), this.size(), this.extra()),
);
}
Selector button[appButton] olduğu için component gerçek bir <button> element'inin üzerine oturuyor; klavye, form ve disabled davranışı tarayıcıdan geliyor. Kartta <button appButton size="sm" [intent]="soldOut() ? 'secondary' : 'primary'" [disabled]="soldOut()"> olarak kullanılıyor. Bu yapının iki kazancı var. Record<ButtonIntent, string> yeni bir intent eklendiğinde eksik kaydı derleme anında yakalıyor; ve bütün class'lar tam adlarıyla kaynakta yazılı olduğu için scanner hepsini görüyor. Aynı işi class-variance-authority (0.7.1) gibi bir kütüphane de yapıyor; Bookstore'da iki seçenekli bir buton için bir Record yetti.
buttonClasses saf bir fonksiyon olduğu için test etmesi kolay:
npx ng test --watch=false
Test Files 2 passed (2)
Tests 4 passed (4)
tailwind-merge: Çakışan Class'ları Birleştirmek
Katalog sayfasındaki "Clear" butonu intent="secondary" size="sm" class="ml-auto px-2" taşıyor. sm boyutu px-3 veriyor, çağıran px-2 istiyor. tailwind-merge, class listesini sondan başa okuyup aynı property grubuna yazan önceki class'ları atıyor:
node scripts/06-merge.mjs
twMerge("bg-brand-600 px-6 py-2", "bg-red-600 px-4") → py-2 bg-red-600 px-4
twMerge("grid", "flex") → flex
twMerge("p-4", "px-2") → p-4 px-2
twMerge("px-2", "p-4") → p-4
twMerge("text-lg text-ink-900", "text-hero") → text-lg text-hero
twMerge("hover:bg-brand-700", "hover:bg-red-700") → hover:bg-red-700
extended: ("text-lg text-ink-900", "text-hero") → text-ink-900 text-hero
İlk satır tahmindeki sorunun çözümü: bg-brand-600 ve px-6 çıktı, çağıranın class'ları kaldı. p-4 px-2'de ikisi de kalıyor, çünkü px-2 yalnız yatay boşluğu eziyor ve CSS'te px-*, p-*'den sonra geliyor; ters sırada ise p-4 hem yatayı hem dikeyi kapsadığı için px-2 atılıyor. Variant'lar ayrı tutuluyor: hover: önekli iki renk birbirini eziyor ama hover:'sız bir renge dokunmuyor.
Beşinci satır asıl tuzak. text-hero, Bookstore'un @theme'de tanımladığı bir font boyutu (--text-hero). tailwind-merge Tailwind'in CSS'ini okumuyor, kendi kuralları var; text- ile başlayan ve tanımadığı bir değeri renk sanıyor. Sonuçta text-ink-900 rengini attı, text-lg font boyutunu bıraktı: tam ters. extendTailwindMerge ile text theme grubuna hero'yu ekleyince doğru sonucu verdi. Kendi @theme namespace'lerine (font boyutu, shadow, radius) değer eklediğinde tailwind-merge config'ine de eklemek gerekiyor; yoksa hata sessiz.
Tarayıcıda ölçtük:
node scripts/06-button.mjs
Clear (intent=secondary size=sm class="ml-auto px-2") → has px-3: false, has px-2: true, padding-left 8px, margin-left auto
Add to cart disabled=false bg oklch(0.54 0.178 264) opacity 1 cursor default
Add to cart disabled=false bg oklch(0.54 0.178 264) opacity 1 cursor default
Notify me disabled=true bg rgb(255, 255, 255) opacity 0.5 cursor not-allowed
Add to cart disabled=false bg oklch(0.54 0.178 264) opacity 1 cursor default
Bedeli de var: merge'i düz bir join(' ') ile değiştirip build aldığımızda main bundle 281.83 kB'tan 253.18 kB'a indi; tailwind-merge yaklaşık 28.6 kB ham, 7.6 kB transfer getiriyor. Tailwind dokümanının önerisi, dışarıdan serbest class kabul etmek yerine component'e anlamlı seçenekler eklemek. Bookstore'da class override'ını yalnız yerleşim class'ları (ml-auto, w-full) için kullanıyoruz; renk ve boyut değişikliği yeni bir intent veya size olarak geliyor.
Prettier ile Class Sıralama
Aynı class'ların her dosyada farklı sırayla yazılması diff'leri okunmaz hâle getiriyor. prettier-plugin-tailwindcss, class'ları Tailwind'in CSS'teki sırasına göre diziyor. v4'te plugin'in temayı ve özel utility'leri bilmesi için tailwindStylesheet vermek gerekiyor:
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./conflict.css",
"printWidth": 140
}
npx prettier 06/unsorted.html
<button
class="custom-legacy rounded-md bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 focus-visible:outline-2 disabled:opacity-50 md:px-6"
>
Add to cart
</button>
Sıra şöyle: Tailwind'in tanımadığı class'lar (custom-legacy) başa, sonra layout ve kutu modeli, tipografi, renk, en sonda variant'lı class'lar. İlk denemede stylesheet'te --color-brand-700 yoktu; plugin hover:bg-brand-700'ü tanımadı ve onu da custom-legacy'nin yanına, en başa koydu. Class'lardan biri beklenmedik biçimde başa kaçıyorsa genellikle ya yazım hatası ya da stylesheet'te olmayan bir token.
bookstore-web'de aynı config'i .prettierrc'ye ekleyip npx prettier --write "src/app/**/*.{html,ts}" çalıştırdık. Angular'ın inline template'leri de işlendi: format filtresindeki class'lar aria-selected:* önce, focus-visible:* sonra yazılmıştı; plugin focus-visible: grubunu öne aldı. TypeScript'teki INTENT ve SIZE string'leri ise yalnız satır uzunluğu için kırıldı, sıralanmadı. Bir fonksiyona verilen string'leri de sıralatmak için plugin'in tailwindFunctions seçeneği var (README'de ["clsx"] örneğiyle); bunu denemedik. Sıralamanın hangi kurala göre yapılacağı bir ekip kararı; kuralın kendisi Coding Conventions ve Ekip Standartları serisinin konusu.
v3'ten v4'e: npx @tailwindcss/upgrade
Bookstore'un eski back office sayfalarından biri hâlâ Tailwind 3.4.19'da: bir tailwind.config.js, üç @tailwind satırı, @layer components içinde @apply ile yazılmış bir buton ve v3 class adları:
module.exports = {
content: ["./index.html"],
theme: {
extend: {
colors: {
brand: { 600: "#3766d4", 700: "#2a53b3" },
},
fontFamily: {
display: ["Georgia", "Cambria", "serif"],
},
},
},
};
<div id="panel" class="mt-4 rounded border p-4 shadow-sm">
<input id="search" class="rounded-md border px-2 py-1" placeholder="Search by ISBN">
<button id="save" class="btn-primary">Save</button>
<span id="badge" class="rounded-full bg-black bg-opacity-50 px-2 text-white">Draft</span>
<img id="cover" class="flex-shrink-0 shadow" src="" alt="" width="40" height="60">
<p id="total" class="!font-bold">Total: 1240.00 TRY</p>
</div>
Resmî upgrade aracı Node.js 20 veya üstü istiyor ve değişiklikleri doğrudan dosyalara yazıyor; bu yüzden temiz bir git çalışma alanında çalıştırdık:
npx @tailwindcss/upgrade@4.3.5
≈ tailwindcss v4.3.5
│ ↳ Upgrading from Tailwind CSS `v3.4.19`
│ Searching for CSS files in the current directory and its subdirectories…
│ ↳ Linked `./tailwind.config.js` to `./src/input.css`
│ Migrating JavaScript configuration files…
│ ↳ Migrated configuration file: `./tailwind.config.js`
│ Migrating stylesheets…
│ ↳ Migrated stylesheet: `./src/input.css`
│ Updating dependencies…
│ ↳ Updated package: `tailwindcss`
│ Migrating templates…
│ ↳ Migrated `./index.html`
│ ↳ Migrated templates for configuration file: `./tailwind.config.js` (1 file changed)
│ ↳ No PostCSS config found, skipping migration.
│ Verify the changes and commit them to your repository.
Template'teki değişiklikler:
- <div id="panel" class="mt-4 rounded border p-4 shadow-sm">
+ <div id="panel" class="mt-4 rounded-sm border p-4 shadow-xs">
- <img id="cover" class="flex-shrink-0 shadow" src="" alt="" width="40" height="60">
- <p id="total" class="!font-bold">Total: 1240.00 TRY</p>
+ <img id="cover" class="shrink-0 shadow-sm" src="" alt="" width="40" height="60">
+ <p id="total" class="font-bold!">Total: 1240.00 TRY</p>
Araç yeniden adlandırılan utility'leri doğru çevirdi: v3'ün rounded'ı v4'te rounded-sm, shadow-sm → shadow-xs, shadow → shadow-sm, flex-shrink-0 → shrink-0 ve önde duran ! sona taşındı. tailwind.config.js'i sildi ve içeriğini @theme'e taşıdı; @layer components içindeki butonu @utility btn-primary'ye çevirdi (focus:outline-none → focus:outline-hidden, focus:ring → focus:ring-3). Varsayılan border renginin gray-200'den currentColor'a değişmesi için de CSS'e açıklamalı bir uyumluluk bloğu ekledi.
Sonra iki sürümü Playwright ile karşılaştırdık; araç her şeyi yakalamamıştı:
| Element | v3.4.19 | Upgrade sonrası v4.3.5 |
|---|---|---|
| --- | --- | --- |
| panel border rengi | rgb(229, 231, 235) | oklch(0.928 0.006 264.531) (uyumluluk bloğu) |
#save cursor | pointer | default |
#save:focus ring | rgba(59, 130, 246, 0.5) 0px 0px 0px 3px | rgb(255, 255, 255) 0px 0px 0px 3px |
#badge arka plan | rgba(0, 0, 0, 0.5) | rgb(0, 0, 0) |
Üç fark var. bg-opacity-50 v4'te kaldırıldı ve araç onu bg-black/50'ye çevirmedi; rozet düz siyah oldu. focus:ring-3 artık 3px ama rengi v3'teki yarı saydam mavi değil currentColor, yani butonun beyaz yazı rengi. Ve v4'ün Preflight'ı butonlara cursor: pointer vermiyor. Üçünü de upgrade rehberindeki karşılıklarıyla elle düzelttik: bg-black/50, focus:ring-3 focus:ring-blue-500/50 ve rehberdeki cursor: pointer bloğu. Son ölçümde üçü de v3'teki gibi. Bir de build kırıldı: package.json'daki tailwindcss -i ... script'i v4'te @tailwindcss/cli paketinde; araç yalnız tailwindcss'i güncellemişti ve izole bir kopyada npm run build "tailwindcss: command not found" verdi. npm i -D @tailwindcss/cli ile çözüldü.
Bu deneyin dersi: upgrade aracı işin büyük kısmını yapıyor, ama "araç bitti, merge et" yeterli değil. Geçişten önce kritik ekranların computed style'larını ya da ekran görüntülerini al, sonra karşılaştır.
Tarayıcı Tabanı ve v3.4'te Kalmak
v4 modern CSS özelliklerine (cascade layers, @property, color-mix()) dayanıyor ve Safari 16.4, Chrome 111 ve Firefox 128'i taban alıyor. Bookstore'un kurumsal müşterilerinden birinin kilitli, eski bir tarayıcıyla çalıştığını düşün: o ekranlar için v4'e geçmek kırık bir arayüz demek. Upgrade rehberi bu durumda v3.4'te kalmayı söylüyor ve npm bunu açıkça destekliyor:
npm view tailwindcss dist-tags --json
{
"next": "4.0.0",
"v3-lts": "3.4.19",
"latest": "4.3.5",
"insiders": "0.0.0-insiders.a3cb19c"
}
v3-lts etiketi 3.4.19'u gösteriyor; bu sürüm 10 Aralık 2025'te yayımlandı. Eski tarayıcı desteği gereken uygulamayı tailwindcss@v3-lts ile sabitleyip yeni uygulamaları v4'te yazmak, aynı monorepo'da bile mümkün; iki uygulama ayrı package.json'lara sahip olduğu sürece.
Sürüm Haritası
npm yayın tarihleri ve Tailwind blog duyurularına göre v4 serisi:
| Sürüm | npm tarihi | Öne çıkanlar |
|---|---|---|
| --- | --- | --- |
| 4.0 | 21 Ocak 2025 | CSS-first configuration, @theme, otomatik içerik taraması, Vite plugin'i, built-in container query, starting:, not-* |
| 4.1 | 1 Nisan 2025 | text-shadow-*, mask-*, @source not, @source inline(), renkli drop-shadow-*, pointer-* variant'ları, wrap-break-word |
| 4.2 | 18 Şubat 2026 | @tailwindcss/webpack, mauve/olive/mist/taupe paletleri, blok yönlü logical utility'ler, font-features-* |
| 4.3 | 8 Mayıs 2026 | scrollbar-*, @container-size, zoom-*, tab-*, stacked ve compound @variant, --default() |
| 4.3.5 | 6 Kasım 2026 | Bu serinin kullandığı patch sürümü |
v4.2 duyurusu bir de deprecation getirdi: start-* ve end-*, inset-s-* ve inset-e-* lehine kullanımdan kalktı. 4.3.5 hâlâ ikisini de üretiyor, ama canonicalize komutu yenisini öneriyor:
npx tailwindcss canonicalize start-0
inset-s-0
Sürüm takibi için iki kaynak yeterli: GitHub releases sayfası ve blog. Minor sürümler yeni utility'ler getiriyor, mevcut class'ları kırmıyor; yine de yükseltmeyi ayrı bir pull request'te yapıp kritik ekranları ölçmek iyi bir alışkanlık.
Üretimde Dikkat
Büyük bir uygulamada v3'ten v4'e geçişi tek seferde yapmak zorunda değilsin. Bookstore'da back office ayrı bir derleme hedefi; önce onu taşıyıp katalog ve checkout'u bir sonraki sürüme bırakmak mümkün. Önemli olan, aynı sayfaya iki Tailwind sürümünün CSS'ini birlikte yüklememek: v3 ve v4'ün .shadow-sm gibi aynı adlı ama farklı değerli class'ları üst üste biner ve hangisinin kazanacağını yine stylesheet sırası belirler.
tailwind-merge'ü kullanıyorsan onun da sürümünü Tailwind'le birlikte düşün. tailwind-merge 3, Tailwind v4'ün class adlarını biliyor; v4.2'nin inset-s-* ya da v4.3'ün scrollbar-* gibi yeni utility'lerinin tailwind-merge tarafından tanınıp tanınmadığını test etmeden varsayma. buttonClasses için yazdığımız birim testleri tam bu yüzden var.
Prettier plugin'ini CI'da prettier --check ile çalıştır. Yerelde editör kaydederken sıralıyor, ama ekipte biri plugin'i kurmamışsa sırasız class'lar repo'ya girer.
Sık Düşülen Tuzaklar
İlki tahmindeki tuzak: çakışan iki utility'den class attribute'unda sonra geleni kazanacak sanmak. Kazanan, CSS'te sonra gelen.
İkincisi tailwind-merge'e özel token'ları öğretmemek. text-hero'yu renk sanıp text-ink-900'ü silmesi hiçbir uyarı üretmedi; buton sadece yanlış renkte göründü.
Üçüncüsü v3 alışkanlıkları: shadow-sm yazıp v3'teki ince gölgeyi beklemek (v4'te bu v3'ün shadow'u), ring yazıp 3px mavi halka beklemek (v4'te 1px ve currentColor), outline-none'ı odak halkasını gizlemek için kullanmak (v4'te gerçekten outline-style: none; erişilebilir hâli outline-hidden) ve zincirli variant'larda v3'ün sağdan sola sırasını kullanmak (v4'te soldan sağa; first:*:pt-0 yerine *:first:pt-0).
Dördüncüsü yeni kodda start-* ve end-* yazmak. Bugün çalışıyorlar ama kullanımdan kalktılar; inset-s-* ve inset-e-* yaz.
Küçük Bir Deneme
★☆☆ 06/conflict.html'e class="p-4 px-2" ve class="px-2 p-4" taşıyan iki element ekle. Önce sonucu tahmin et, sonra 06-conflict.mjs ile computed padding'i ölç ve tailwind-merge'ün aynı iki listeye ne dediğiyle karşılaştır.
★★☆ bookstore-web'deki butona ghost adında dördüncü bir intent ekle (bg-transparent, hover'da bg-slate-100). Record tipinin eksik kaydı nasıl yakaladığını gör, sonra buttonClasses için bir birim testi yaz.
★★★ 06-v3 klasörünü yazıdaki v3 hâline geri döndür (tailwind.config.js'i ve eski class'ları yukarıdaki kod bloklarından al, tailwindcss@3.4.19 kur), şablona ring-offset-2, divide-y ve space-y-4 kullanan bir liste ekle, upgrade aracını tekrar çalıştır ve 06-legacy.mjs'i genişleterek aracın bu üçünde neyi değiştirdiğini ölç.
Serinin bu bölümünde Tailwind'in kendisini kurduk: yaklaşım, configuration, token'lar, variant'lar, Angular ve ASP.NET Core entegrasyonu ve bakım. Sıradaki iki yazıda her gün yazdığımız utility ailelerine geçiyoruz; ilkinde layout, flex, grid, spacing, sizing ve position utility'lerini, ürettikleri CSS'le yan yana koyarak Bookstore'un katalog grid'i, kitap kartı ve sepet sayfası üzerinden okuyacağız.