@sedatdemirAra

CSS :has() ile 10 pratik desen

Bir <input> geçersiz olduğunda label rengini kırmızıya çevirmek, uzun yıllar boyunca ya JavaScript ya da kardeş seçicileriyle yapılan bir numara gerektirdi. Çünkü CSS'te bir ebeveyn, çocuğunun durumuna göre stil alamıyordu. :has() bunu tamamen değiştirdi. Chromium 105 (Eylül 2022), Safari 15.4 ve Firefox 121 (Aralık 2023) ile birlikte artık tüm modern tarayıcılar bu seçiciyi destekliyor.

Aşağıda günlük projelerde doğrudan kullanılabilecek 10 desen var. Her biri kısa ve kopyalanıp yapıştırılabilir.

1. Form alanı geçersizken label stilini değiştirme

Klasik senaryo: kullanıcı hatalı bir değer girdiğinde hem input'un hem de label'ın görsel olarak "hatalı" duruma geçmesi.

.form-group:has(input:invalid) label {
  color: #d32f2f;
}

.form-group:has(input:invalid) input {
  border-color: #d32f2f;
  outline-color: #d32f2f;
}
<div class="form-group">
  <label for="email">E-posta</label>
  <input id="email" type="email" required />
</div>

Burada .form-group, çocuğundaki input:invalid durumunu :has() ile algılıyor. JavaScript yok, class toggle yok.

2. Checkbox işaretlendiğinde kartı vurgulama

.card:has(input[type="checkbox"]:checked) {
  background-color: #e3f2fd;
  border-color: #1976d2;
}

Bu desen, seçilebilir kart listelerinde çok işe yarıyor. Checkbox'ın kartın neresinde olduğu fark etmez; :has() DOM derinliğine bakar.

3. Boş sepet durumu

.cart:has(.cart-item) .empty-message {
  display: none;
}

.cart:not(:has(.cart-item)) .empty-message {
  display: block;
}

Sepette en az bir .cart-item varsa "Sepetiniz boş" mesajı gizleniyor. JavaScript ile eleman sayısını kontrol edip class eklemek yerine CSS tek başına hallediyor.

4. Tablo satırını hover'da vurgulama (hücre bazlı)

Klasik tr:hover zaten mümkün ama bazen belirli bir hücredeki duruma göre tüm satırı farklı boyamak gerekir:

tr:has(td.status--error) {
  background-color: #fce4ec;
}

tr:has(td:hover) {
  background-color: #f5f5f5;
}

İlk kural, içinde .status--error sınıflı bir <td> bulunan satırları kalıcı olarak kırmızımsı boyar. İkincisi, herhangi bir hücresine hover yapılan satırı boyar.

5. Resim içeren ve içermeyen kart düzenleri

Aynı .card bileşenini hem resimli hem resimsiz içerikle kullanırken düzen değiştirmek:

.card:has(img) {
  grid-template-columns: 200px 1fr;
}

.card:not(:has(img)) {
  grid-template-columns: 1fr;
  padding: 1.5rem;
}

Bu yaklaşım, ayrı bir .card--with-image modifier sınıfı oluşturma ihtiyacını ortadan kaldırıyor.

6. Quantity queries: eleman sayısına göre düzen

Quantity query'ler :has() öncesinde :nth-last-child ile mümkündü ama okunması zordu. :has() ile çok daha net yazılıyor.

4 veya daha fazla öğe varsa grid'e geç

.tag-list:has(:nth-child(4)) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 0.5rem;
}

.tag-list:not(:has(:nth-child(4))) {
  display: flex;
  gap: 0.5rem;
}

Mantık şu: :nth-child(4) en az 4. çocuğun var olup olmadığını kontrol eder. Eğer .tag-list içinde en az 4 çocuk varsa grid düzenine geçiyoruz, yoksa yatay flex kalıyor.

Tek öğe varsa genişlet

.feature-list:has(:only-child) .feature-item {
  max-width: 100%;
  text-align: center;
}

Bu tür quantity query'ler, bileşen kütüphanelerinde çok değerli. İçerik sayısına bağlı koşullu düzeni saf CSS'e taşıyorlar.

7. Açık dialog/modal varken scroll kilitleme

html:has(dialog[open]) {
  overflow: hidden;
}

Bu tek satır, sayfada herhangi bir <dialog> açıkken body scroll'unu kilitler. JavaScript'le document.body.classList.add('no-scroll') yapmaya gerek kalmıyor. Popover API ve dialog yazısında bahsettiğim native dialog kullanımıyla birleştirildiğinde güçlü bir kombinasyon oluşturuyor.

8. Focus-within alternatifi: iç içe focus takibi

:focus-within zaten ebeveyn seçimi yapabilir ama :has() ile daha spesifik hedefleme mümkün:

/* Sadece textarea'ya focus geldiğinde, submit butonunu göster */
.comment-box:has(textarea:focus) .submit-btn {
  opacity: 1;
  transform: translateY(0);
}

.comment-box .submit-btn {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s, transform 0.2s;
}

:focus-within kullansaydık, submit butonunun kendisine focus geldiğinde de tetiklenirdi. :has(textarea:focus) yalnızca textarea'ya focus gelince çalışır.

9. Karanlık/aydınlık mod geçişi (class-free)

html:has(#theme-toggle:checked) {
  color-scheme: dark;
  --bg: #121212;
  --text: #e0e0e0;
}

body {
  background: var(--bg, #ffffff);
  color: var(--text, #1a1a1a);
}
<input type="checkbox" id="theme-toggle" hidden />
<label for="theme-toggle">Karanlık mod</label>

JavaScript yazmadan, checkbox durumuna bağlı olarak tüm sayfanın renk şeması değişiyor. Küçük projeler ve statik siteler için yeterli bir çözüm.

10. Yan yana sidebar açık/kapalı düzeni

.layout:has(.sidebar[data-open="true"]) .main-content {
  margin-left: 260px;
}

.layout:not(:has(.sidebar[data-open="true"])) .main-content {
  margin-left: 0;
}

Sidebar'ın açık olup olmadığı bir data-open attribute'üne bağlı. JavaScript yalnızca bu attribute'ü toggle ediyor; düzen değişikliğini CSS hallediyor. CSS if() ve style queries ile birlikte düşünüldüğünde, koşullu stil yazmanın CSS tarafında ne kadar güçlendiğini görmek mümkün.

Performans notları

:has() kullanırken performans endişesi sık dile gelir. Haklı sebepleri var: geleneksel CSS seçicileri sağdan sola değerlendirilir ve ebeveyn sorgulaması bu modele aykırıdır. Tarayıcı motorları :has() için özel optimizasyonlar uyguladı.

Chromium ekibi, Blink motorundaki :has() implementasyonunda "bloom filter" tabanlı bir ön eleme katmanı kullandığını belgeledi (Blink-dev tartışması, 2022). Bu sayede tarayıcı, :has() içindeki alt seçiciyle eşleşme ihtimali olmayan elemanları hızlıca eliyor.

Birkaç pratik kural:

  • :has() içindeki seçiciyi olabildiğince dar tutun. div:has(span) yerine .card:has(.card-icon) yazın. Genel element seçicileri tarama alanını genişletir.
  • İç içe :has() kullanımı (A:has(B:has(C))) kaçınılması gereken bir kalıp. Tarayıcı bunu değerlendirebilir ama maliyet katlanır.
  • *:has(...) hiçbir zaman yazmayın. Her elemanda ebeveyn sorgusu çalıştırmak, büyük DOM ağaçlarında layout süresini gözle görülür şekilde artırır.
  • Scroll ile tetiklenen animasyonlar sırasında :has() tabanlı stil değişikliği, reflow'u tetikleyebilir. Scroll-driven animations yazısındaki yaklaşımlarla birleştirirken dikkatli olun.

Kendi testlerimde, 1000 satırlık bir tabloda tr:has(td.highlight) seçicisi Chrome 130'da herhangi bir fark edilir gecikme yaratmadı. Ancak 5000+ DOM node'lu bir sayfada *:has(input:checked) gibi geniş bir seçici, DevTools Performance panelinde "Recalculate Style" süresini 2-3 ms'den 15+ ms'ye çıkardı. Seçiciyi .form-section:has(input:checked) olarak daraltmak bu süreyi tekrar 3 ms civarına indirdi.

:has() ve diğer modern CSS özellikleri

:has() tek başına güçlü ama CSS anchor positioning ve @starting-style gibi yeni özelliklerle birleştirildiğinde daha önce JavaScript gerektiren pek çok etkileşim kalıbı saf CSS'e taşınabiliyor.

Örneğin bir tooltip'in yalnızca içeriği olan elementlerde gösterilmesi:

.tooltip-trigger:has([data-tooltip]:not([data-tooltip=""])):hover::after {
  content: attr(data-tooltip);
  position: absolute;
  /* ... tooltip stilleri */
}

Bu tür kombinasyonlar, bileşen kütüphanelerindeki modifier class sayısını ciddi şekilde azaltıyor.

:has() seçicisini bugün production'da güvenle kullanabilirsiniz; Web Baseline 2026 listesinde "widely available" olarak işaretli. Yukarıdaki 10 desenden birini mevcut projenize eklediğinizde, muhtemelen birkaç satır JavaScript'i silebileceksiniz. Performans konusunda tek hatırlanması gereken şey: seçiciyi dar tutun, *:has(...) yazmayın.