2026'da Modern CSS: Stil Yazma Şeklimi Değiştiren Özellikler
Container query, CSS layer, :has(), color-mix(), nesting, scroll-driven animasyon ve anchor positioning. JavaScript'e uzanmayı bıraktıran CSS özellikleri.
Son Sass dosyamı altı ay önce sildim. Bir manifesto yayınlamak için değil. Gerçekten artık ihtiyacım olmadığı için.
On yılı aşkın bir süre boyunca CSS, özür dilediğimiz bir dildi. Nesting ve değişkenler için ön işlemcilere ihtiyacımız vardı. Container bazlı boyutlandırma, scroll bağlantılı animasyonlar, parent seçimi ve tasarımcıların bize verdiği layout kalıplarının yarısı için JavaScript'e ihtiyacımız vardı. Dilin yerel olarak yapamadığı şeyleri telafi etmek için bütün çalışma zamanı sistemleri kurduk — CSS-in-JS kütüphaneleri, utility framework'ler, PostCSS eklenti zincirleri.
O dönem bitti. "Neredeyse bitti" değil. "Yolda" değil. Bitti.
2024 ile 2026 arasında tarayıcılara gelen özellikler sadece kolaylık eklemedi. Zihinsel modeli değiştirdi. CSS artık savaştığın bir stil dili değil. Bileşenleri, specificity yönetimini, element düzeyinde duyarlı tasarımı ve runtime olmadan animasyonu gerçekten düşünen bir stil dili.
İşte neler değişti, neden önemli ve bunlar sayesinde artık nelerden vazgeçebilirsin.
Container Query: "Viewport Ne Kadar Geniş?" Döneminin Sonu#
Bu, duyarlı tasarım hakkındaki düşünme biçimimi temelden değiştiren özellik. Yirmi yıl boyunca media query'lerimiz vardı. Media query'ler sorar: "viewport ne kadar geniş?" Container query'ler sorar: "bu bileşenin içinde yaşadığı container ne kadar geniş?"
Bu ayrım ince görünüyor. Değil. Sadece tek bir layout bağlamında çalışan bileşenler ile her yerde çalışan bileşenler arasındaki fark bu.
Yirmi Yıldır Var Olan Problem#
Bir kart bileşeni düşün. Sidebar'da dikey düzenli küçük bir görsel ile yığılmalı. Ana içerik alanında yatay düzenli daha büyük bir görselle olmalı. Tam genişlikli bir hero bölümünde tamamen başka bir şey olmalı.
Media query'ler ile şöyle bir şey yazardın:
/* The old way: coupling component styles to page layout */
.card {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.sidebar .card {
/* Still vertical in sidebar */
}
.main-content .card {
flex-direction: row;
/* Horizontal in main area */
}
}
@media (min-width: 1200px) {
.hero .card {
/* Yet another layout */
}
}Kart .sidebar, .main-content ve .hero'yu biliyor. Sayfayı biliyor. Artık bir bileşen değil — sayfaya bağımlı bir parça. Başka bir sayfaya taşı ve her şey bozulur.
Container Query Bunu Tamamen Çözüyor#
/* The container query way: component knows only about itself */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@container card (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
}
}
@container card (min-width: 700px) {
.card {
grid-template-columns: 300px 1fr;
gap: 2rem;
font-size: 1.125rem;
}
}Kart nerede yaşadığını bilmiyor. Umursamıyor. 300px'lik bir sidebar'a koy, dikey olur. 700px'lik bir ana alana koy, yatay olur. Tam genişlikli bir bölüme bırak, uyum sağlar. Sayfa layout'u hakkında sıfır bilgi gerekli.
inline-size vs size#
Neredeyse her zaman container-type: inline-size isteyeceksin. Bu, inline eksen (yatay yazma modlarında genişlik) üzerinde sorgulamayı etkinleştirir. container-type: size kullanmak hem inline hem de blok ekseni sorgularını etkinleştirir, ama container'ın her iki boyutta da açık bir boyutlandırmaya sahip olmasını gerektirir, ki bu çoğu durumda normal belge akışını bozar.
/* This is what you want 99% of the time */
.wrapper {
container-type: inline-size;
}
/* This requires explicit height — rarely what you want */
.wrapper-both {
container-type: size;
height: 500px; /* required, or it collapses */
}İç İçe Bağlamlarda İsimli Container'lar#
Container'ları iç içe koyduğunda isimlendirme zorunlu hale gelir:
.page-layout {
container-type: inline-size;
container-name: layout;
}
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
/* Target the sidebar specifically, not the nearest ancestor */
@container sidebar (max-width: 250px) {
.nav-item {
font-size: 0.875rem;
padding: 0.25rem;
}
}
/* Target the page layout */
@container layout (min-width: 1200px) {
.page-header {
font-size: 2.5rem;
}
}İsim olmadan @container, containment'a sahip en yakın atayı sorgular. Nesting'de bu genellikle istediğin container değildir. İsimlendir. Her zaman.
Container Query Birimleri#
Bu hafife alınıyor. Container query birimleri (cqi, cqb, cqw, cqh) şeyleri viewport yerine container'a göre boyutlandırmana izin verir:
@container (min-width: 400px) {
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
}4cqi, container'ın inline boyutunun %4'ü. Başlık pencere ile değil, container ile ölçeklenir. Akışkan tipografi başından beri böyle olmalıydı.
CSS Layer: Specificity Savaşı Bitti#
Container query'ler duyarlı tasarım hakkındaki düşüncelerimi değiştirdiyse, @layer CSS mimarisi hakkındaki düşüncelerimi değiştirdi. İlk kez, tüm proje boyunca specificity'yi yönetmenin makul, deklaratif bir yolu var.
Problem#
CSS specificity, niyetlerini umursamayan bir puan sistemidir. .text-red utility sınıfı .card .title'a yenilir çünkü ikincisinin specificity'si daha yüksek. Çözüm her zaman aynıydı: seçicilerini daha spesifik yap, !important ekle veya her şeyi yeniden yapılandır.
Sadece specificity çakışmalarını önlemek için bütün metodolojiler (BEM, SMACSS, ITCSS) ve araç zincirleri kurduk. Bunların hepsi eksik bir dil özelliğinin geçici çözümüydü.
Layer Sıralaması#
@layer, stil gruplarının hangi sırayla değerlendirileceğini, bu gruplar içindeki specificity'den bağımsız olarak belirlememizi sağlar:
/* Declare layer order — this single line controls everything */
@layer reset, base, components, utilities, overrides;
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: oklch(20% 0 0);
}
h1, h2, h3 {
line-height: 1.2;
text-wrap: balance;
}
}
@layer components {
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px oklch(0% 0 0 / 0.12);
}
.card .title {
font-size: 1.25rem;
font-weight: 600;
color: oklch(25% 0.05 260);
}
}
@layer utilities {
.text-red {
color: oklch(55% 0.25 25);
}
.hidden {
display: none;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
}.card .title'ın specificity'si .text-red'den yüksek olmasına rağmen, utility kazanır çünkü utilities layer'ı components'tan sonra tanımlanmıştır. !important yok. Specificity hileleri yok. Layer sırası son sözü söyler.
Tailwind CSS v4 Layer'ları Nasıl Kullanıyor#
Tailwind v4, @layer üzerine yoğun şekilde dayanır. @import "tailwindcss" yazdığında şunu alırsın:
@layer theme, base, components, utilities;Her Tailwind utility utilities layer'ında yaşar. Özel bileşen stillerinin components'a gider. text-red-500 sınıfının bir bileşenin rengini !important olmadan geçersiz kılabilmesinin nedeni budur — daha sonraki bir layer'dadır.
Tailwind olmadan kendi tasarım sistemini kuruyorsan, bu mimariyi çal. Doğru olan budur:
@layer reset, tokens, base, layouts, components, utilities, overrides;Yedi layer yeterli. Daha fazlasına hiç ihtiyacım olmadı.
Layer'sız Stiller Her Şeyi Yener#
Bir tuzak: herhangi bir layer'da olmayan stiller en yüksek önceliğe sahiptir. Bu aslında faydalıdır — tek seferlik sayfa özelindeki geçersiz kılmaların otomatik olarak kazanması demektir:
@layer components {
.modal {
background: white;
}
}
/* Not in any layer — wins over everything in layers */
.special-page .modal {
background: oklch(95% 0.02 260);
}Ama bu aynı zamanda layer farkındalığı olmayan üçüncü parti CSS'in tüm sistemini geçersiz kılabileceği anlamına da gelir. Üçüncü parti stilleri kontrol etmek için bir layer'a sar:
@layer third-party {
@import url("some-library.css");
}:has() Seçicisi: Her Zaman İstediğimiz Parent Seçici#
Kelimenin tam anlamıyla on yıllardır geliştiriciler bir parent seçici istedi. "Bir parent'ı çocuklarına göre stillemek istiyorum." Cevap her zaman "CSS bunu yapamaz" ve ardından bir JavaScript geçici çözümüydü. :has() bunu tamamen değiştiriyor ve istediğimizden bile daha güçlü olduğu ortaya çıkıyor.
Temel Parent Seçimi#
/* Style a form group when its input has focus */
.form-group:has(input:focus) {
border-color: oklch(55% 0.2 260);
box-shadow: 0 0 0 3px oklch(55% 0.2 260 / 0.15);
}
/* Style a card differently when it contains an image */
.card:has(img) {
padding-top: 0;
}
.card:has(img) .card-content {
padding: 1.5rem;
}
/* A card without an image gets different treatment */
.card:not(:has(img)) {
border-left: 4px solid oklch(55% 0.2 260);
}JavaScript Olmadan Form Doğrulama Durumları#
:has()'in gerçekten heyecan verici olduğu yer burası. HTML doğrulama pseudo-class'ları ile birleştirildiğinde, sıfır JavaScript ile validity durumuna tepki veren form UI'ları oluşturabilirsin:
/* The field wrapper reacts to its input's validity */
.field:has(input:invalid:not(:placeholder-shown)) {
--field-color: oklch(55% 0.25 25);
}
.field:has(input:valid:not(:placeholder-shown)) {
--field-color: oklch(55% 0.2 145);
}
.field {
--field-color: oklch(70% 0 0);
border: 2px solid var(--field-color);
border-radius: 0.5rem;
padding: 0.75rem;
transition: border-color 0.2s;
}
.field label {
color: var(--field-color);
font-size: 0.875rem;
font-weight: 500;
}
.field .error-message {
display: none;
color: oklch(55% 0.25 25);
font-size: 0.8rem;
margin-top: 0.25rem;
}
.field:has(input:invalid:not(:placeholder-shown)) .error-message {
display: block;
}:not(:placeholder-shown) kısmı kritik — henüz dokunulmamış boş alanlarda doğrulama stillerinin görünmesini engeller.
Çocuk Sayısına Göre Layout#
Bu kalıp saçma derecede kullanışlı ve :has() öncesinde gerçekten imkansızdı:
/* Adjust grid columns based on how many items exist */
.grid-auto:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid-auto:has(> :nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
.grid-auto:has(> :nth-child(13)) {
grid-template-columns: repeat(4, 1fr);
}Grid, kaç çocuğa sahip olduğuna göre sütun sayısını değiştirir. JavaScript yok. ResizeObserver yok. Sınıf değiştirme yok.
Sidebar Algılama#
En sevdiğim kalıplardan biri — sidebar'ın var olup olmadığına göre layout'u değiştirmek:
.page-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
/* If the layout contains a sidebar, switch to two columns */
.page-layout:has(.sidebar) {
grid-template-columns: 1fr 300px;
}
/* Adjust main content width when sidebar is present */
.page-layout:has(.sidebar) .main-content {
max-width: 65ch;
}DOM'a bir sidebar bileşeni ekle, layout uyum sağlar. Kaldır, geri uyum sağlar. CSS gerçeğin kaynağıdır, bir state değişkeni değil.
:has()'i Diğer Seçicilerle Birleştirmek#
:has() diğer her şeyle güzelce bileşir:
/* Style an article only when it has a specific class of figure */
article:has(figure.full-bleed) {
overflow: visible;
}
/* A navigation that changes when it contains a search input */
nav:has(input[type="search"]:focus) {
background: oklch(98% 0 0);
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.08);
}
/* Enable dark-mode at the component level */
.theme-switch:has(input:checked) ~ main {
color-scheme: dark;
background: oklch(15% 0 0);
color: oklch(90% 0 0);
}Tarayıcı desteği artık mükemmel. Her modern tarayıcı 2024'ün başından beri :has()'i destekliyor. Kullanmamak için hiçbir neden yok.
CSS Nesting: Native, Sonunda Kararlı#
Bunu abartmayacağım. CSS nesting güzel. Container query'ler veya :has() gibi devrimsel değil. Ama Sass kullanmanın son nedenlerinden birini ortadan kaldırıyor ve bu önemli.
Sözdizimi#
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
.title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.description {
color: oklch(40% 0 0);
line-height: 1.7;
}
&:hover {
box-shadow: 0 4px 12px oklch(0% 0 0 / 0.1);
}
&.featured {
border: 2px solid oklch(60% 0.2 260);
}
}Bu geçerli CSS. Build adımı yok. Ön işlemci yok. PostCSS eklentisi yok. Tarayıcı bunu yerel olarak işler.
Sass'tan Farkları#
Bilinmesi gereken birkaç sözdizimi farkı var:
/* CSS Nesting — works now in all browsers */
.parent {
/* Direct class/element nesting works without & */
.child {
color: red;
}
/* & is required for pseudo-classes and compound selectors */
&:hover {
background: blue;
}
&.active {
font-weight: bold;
}
/* Nested media queries — this is great */
@media (width >= 768px) {
flex-direction: row;
}
}İlk uygulamalarda element seçicilerin önünde & gerekiyordu (p yerine & p gibi). Bu kısıtlama kaldırıldı. 2025 itibariyle tüm büyük tarayıcılar çıplak element nesting'i destekliyor: .parent { p { ... } } gayet çalışıyor.
İç İçe Media Query'ler#
Bence CSS nesting'in asıl güçlü özelliği bu. Seçici nesting değil — media query'leri bir kural bloğunun içine koyabilme yeteneği:
.hero {
padding: 2rem;
font-size: 1rem;
@media (width >= 768px) {
padding: 4rem;
font-size: 1.25rem;
}
@media (width >= 1200px) {
padding: 6rem;
font-size: 1.5rem;
}
}Bunu eski yöntemle karşılaştır: .hero stillerinin üç farklı @media bloğuna dağıldığı, muhtemelen yüzlerce satır uzaklıktaki yapı. Nesting, duyarlı davranışı bileşenle birlikte tutar. Okunabilirlik çarpıcı biçimde artar.
Aşırı İç İçe Geçirme#
Bir uyarı: altı seviye derine nesting yapabilmen yapman gerektiği anlamına gelmez. Sass'taki aynı tavsiye burada da geçerli. Eğer nesting'in .page .section .card .content .text .highlight gibi seçiciler oluşturuyorsa, bir specificity canavarı ve bakım kabusu yaratmışsın. İki veya üç seviye ideal nokta.
/* Good — two levels */
.nav {
.link {
color: inherit;
&:hover {
color: oklch(55% 0.2 260);
}
}
}
/* Bad — specificity nightmare */
.page {
.layout {
.sidebar {
.nav {
.list {
.item {
.link {
color: red; /* good luck overriding this */
}
}
}
}
}
}
}Renk Fonksiyonları: oklch() ve color-mix() Her Şeyi Değiştiriyor#
hsl() iyi bir dönem geçirdi. rgb()'den daha sezgiseldi. Ama temel bir kusuru var: algısal olarak tek tip değil. hsl(60, 100%, 50%) (sarı) insan gözüne hsl(240, 100%, 50%)'den (mavi) çarpıcı biçimde daha açık görünür, aynı lightness değerine sahip olmalarına rağmen.
oklch() Neden Kazanıyor#
oklch() algısal olarak tek tip. Eşit lightness değerleri eşit derecede açık görünür. Bu, renk paletleri oluştururken, temalar yaratırken ve erişilebilir kontrast sağlarken son derece önemli:
:root {
/* oklch(lightness chroma hue) */
--color-primary: oklch(55% 0.2 260); /* blue */
--color-secondary: oklch(55% 0.2 330); /* purple */
--color-success: oklch(55% 0.2 145); /* green */
--color-danger: oklch(55% 0.25 25); /* red */
--color-warning: oklch(75% 0.18 85); /* yellow — higher L for equal perception */
/* These all look equally "medium" to the human eye */
/* With HSL, you'd need different lightness values for each hue */
}Üç değer anlaşıldığında sezgiseldir:
- Lightness (0% ile 100%): Ne kadar açık veya koyu. 0% siyah, 100% beyaz.
- Chroma (0 ile ~0.37): Ne kadar canlı. 0 gri, daha yüksek daha doygun.
- Hue (0 ile 360): Renk açısı. 0/360 pembemsi kırmızı, 145 yeşil, 260 mavi.
Türetilmiş Renkler İçin color-mix()#
color-mix(), çalışma zamanında diğer renklerden renk oluşturmana izin verir. Sass darken() fonksiyonu yok. JavaScript yok. Sadece CSS:
:root {
--brand: oklch(55% 0.2 260);
/* Lighten by mixing with white */
--brand-light: color-mix(in oklch, var(--brand) 30%, white);
/* Darken by mixing with black */
--brand-dark: color-mix(in oklch, var(--brand) 70%, black);
/* Create a subtle background */
--brand-bg: color-mix(in oklch, var(--brand) 8%, white);
/* Semi-transparent version */
--brand-overlay: color-mix(in oklch, var(--brand) 50%, transparent);
}
.button {
background: var(--brand);
color: white;
&:hover {
background: var(--brand-dark);
}
&:active {
background: color-mix(in oklch, var(--brand) 60%, black);
}
}
.button.secondary {
background: var(--brand-bg);
color: var(--brand-dark);
&:hover {
background: color-mix(in oklch, var(--brand) 15%, white);
}
}in oklch kısmı önemli. srgb'de karıştırmak bulanık ara renkler verir. oklch'de karıştırmak algısal olarak düzgün sonuçlar verir. Her zaman oklch'de karıştır.
oklch() ile Tam Palet Oluşturmak#
Tek bir hue'dan tüm bir ton paletini şöyle oluşturuyorum:
:root {
--hue: 260;
--chroma: 0.2;
--color-50: oklch(97% calc(var(--chroma) * 0.1) var(--hue));
--color-100: oklch(93% calc(var(--chroma) * 0.2) var(--hue));
--color-200: oklch(85% calc(var(--chroma) * 0.4) var(--hue));
--color-300: oklch(75% calc(var(--chroma) * 0.6) var(--hue));
--color-400: oklch(65% calc(var(--chroma) * 0.8) var(--hue));
--color-500: oklch(55% var(--chroma) var(--hue));
--color-600: oklch(48% var(--chroma) var(--hue));
--color-700: oklch(40% calc(var(--chroma) * 0.9) var(--hue));
--color-800: oklch(32% calc(var(--chroma) * 0.8) var(--hue));
--color-900: oklch(25% calc(var(--chroma) * 0.7) var(--hue));
--color-950: oklch(18% calc(var(--chroma) * 0.5) var(--hue));
}--hue'yu 145'e çevir, yeşil bir paletin olur. 25'e çevir, kırmızı olur. Lightness adımları algısal olarak eşit. Chroma uçlarda azalır böylece en açık ve en koyu tonlar aşırı doygun olmaz. Bu, eskiden bir tasarım aracı veya Sass fonksiyonu gerektiren türde bir şeydi. Şimdi sekiz satır CSS.
Scroll-Driven Animasyonlar: JavaScript Gerekmiyor#
En çok JavaScript silmeme neden olan özellik bu. Scroll bağlantılı animasyonlar — ilerleme çubukları, parallax efektleri, reveal animasyonları, geçişli sticky header'lar — eskiden IntersectionObserver, scroll olay dinleyicileri veya GSAP gibi bir kütüphane gerektirirdi. Şimdi CSS.
Scroll İlerleme Göstergesi#
Bir makale sayfasının üstündeki klasik "okuma ilerleme çubuğu":
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: oklch(55% 0.2 260);
transform-origin: left;
z-index: 1000;
animation: grow-progress linear both;
animation-timeline: scroll();
}
@keyframes grow-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}Bu kadar. Tüm okuma ilerleme göstergesi. JavaScript yok. Scroll olay dinleyicisi yok. requestAnimationFrame yok. "Yüzde kaç scroll edildi" hesaplaması yok. animation-timeline: scroll() bağlantısı her şeyi yapıyor.
Scroll'da Reveal#
Viewport'a girerken beliren elementler:
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}animation-timeline: view() animasyonu elementin viewport'taki görünürlüğüne bağlar. animation-range: entry 0% entry 100% animasyonun elementin viewport'a girmeye başladığı andan tamamen görünür olduğu ana kadar çalışması anlamına gelir.
Kütüphanesiz Parallax#
.parallax-bg {
animation: parallax linear both;
animation-timeline: scroll();
}
@keyframes parallax {
from {
transform: translateY(-20%);
}
to {
transform: translateY(20%);
}
}
.parallax-section {
overflow: hidden;
position: relative;
}Arka plan görseli scroll'dan farklı bir hızda hareket ederek parallax efekti oluşturur. Akıcı, performanslı (tarayıcı GPU'da birleştirebilir) ve sıfır JavaScript.
İsimli Scroll Timeline'ları#
Daha fazla kontrol için scroll timeline'ları isimlendirebilir ve diğer elementlerden referans edebilirsin:
.scroller {
overflow-y: auto;
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: block;
}
/* An element anywhere in the DOM can reference this timeline */
.indicator {
animation: progress linear both;
animation-timeline: --my-scroller;
}
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}Bu, gösterge ile kaydırıcı parent/child olmasa bile çalışır. Herhangi bir element herhangi bir isimli scroll timeline'ına bağlanabilir. Bu, kaydırılabilir bir panelin sabit bir header'daki göstergeyi yönlendirdiği dashboard UI'ları için güçlüdür.
Anchor Positioning: Tooltip ve Popover'lar Düzgün Yapılıyor#
Anchor positioning'den önce, bir tooltip'i tetikleyici elementine bağlamak JavaScript gerektirirdi. getBoundingClientRect() ile pozisyonları hesaplar, scroll offset'lerini yönetir, viewport çakışmalarını idare eder ve resize'da yeniden hesaplardın. Popper.js (şimdi Floating UI) gibi kütüphaneler tam olarak bunun bu kadar zor olması nedeniyle vardı.
CSS anchor positioning bunu deklaratif yapar:
.trigger {
anchor-name: --my-trigger;
}
.tooltip {
position: fixed;
position-anchor: --my-trigger;
/* Position the tooltip's top-center at the trigger's bottom-center */
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
/* Fallback positioning if it overflows the viewport */
position-try-fallbacks: flip-block, flip-inline;
}Otomatik Viewport Çakışma Yönetimi#
position-try-fallbacks özelliği, JavaScript'te 200 satır alacak olan kısım. Tarayıcıya şunu söyler: "tooltip alttan taşarsa, üste çevir. Sağdan taşarsa, sola çevir."
.dropdown-menu {
position: fixed;
position-anchor: --menu-button;
/* Default: below the button, aligned to the left edge */
top: anchor(bottom);
left: anchor(left);
/* If it doesn't fit below, try above. If it doesn't fit left-aligned, try right-aligned */
position-try-fallbacks: flip-block, flip-inline;
/* Add a gap between the anchor and the dropdown */
margin-top: 4px;
}İsimli Pozisyon Geri Dönüşleri#
Geri dönüş pozisyonları üzerinde daha fazla kontrol için özel try seçenekleri tanımlayabilirsin:
@position-try --above-right {
bottom: anchor(top);
right: anchor(right);
top: auto;
left: auto;
margin-top: 0;
margin-bottom: 4px;
}
@position-try --left-center {
right: anchor(left);
top: anchor(center);
left: auto;
bottom: auto;
translate: 0 -50%;
margin-top: 0;
margin-right: 4px;
}
.tooltip {
position: fixed;
position-anchor: --trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
margin-top: 4px;
position-try-fallbacks: --above-right, --left-center;
}Tarayıcı, elementi viewport içinde tutan birini bulana kadar her geri dönüşü sırayla dener. Bu, JavaScript'te gerçekten acı veren türde bir mekansal akıl yürütme.
Popover API ile Anchoring#
Anchor positioning, yeni Popover API ile mükemmel uyum sağlar:
<button popovertarget="my-popover" style="anchor-name: --btn">Settings</button>
<div popover id="my-popover" style="
position-anchor: --btn;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
margin-top: 8px;
">
Popover content here
</div>Gösterme/gizleme için JavaScript yok (Popover API bunu halleder). Konumlandırma için JavaScript yok (anchor positioning bunu halleder). Light-dismiss davranışı için JavaScript yok (Popover API bunu da halleder). Tüm tooltip/popover/dropdown kalıbı — npm paketlerini besleyen bir kalıp — artık HTML ve CSS.
CSS Grid Subgrid: Gerçekten Çalışan İç İçe Hizalama#
Grid güçlüdür, ama bir sinir bozucu sınırlaması vardı: bir çocuk grid, öğelerini parent grid'e hizalayamıyordu. Bir sıra kart varsa ve her kartın başlığı, içeriği ve alt bilgisinin kartlar arasında hizalanmasını istiyorsan, şansın yoktu. Her kartın dahili grid'i bağımsızdı.
Subgrid bunu düzeltiyor.
Kart Hizalama Problemi#
/* Parent grid */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
/* Each card becomes a subgrid, aligning rows to the parent */
.card {
display: grid;
grid-row: span 3; /* Card spans 3 rows in the parent */
grid-template-rows: subgrid;
gap: 0; /* Card controls its own internal gap */
}
.card-title {
font-weight: 600;
padding: 1rem 1rem 0.5rem;
}
.card-body {
padding: 0 1rem;
color: oklch(40% 0 0);
}
.card-footer {
padding: 0.5rem 1rem 1rem;
margin-top: auto;
border-top: 1px solid oklch(90% 0 0);
}Artık üç kartın tamamının başlıkları aynı satırda hizalanır. Gövdeler hizalanır. Alt bilgiler hizalanır. Bir kartın iki satırlık başlığı, diğerinin tek satırlık başlığı olsa bile, hizalama parent grid tarafından korunur.
Subgrid Öncesi: Hackler#
Subgrid olmadan bu hizalamayı sağlamak şunları gerektirirdi:
- Sabit yükseklikler (kırılgan, dinamik içerikle bozulur)
- JavaScript ölçümü (yavaş, yanlış hizalama anlık görünümü)
- Vazgeçip yanlış hizalamayı kabul etmek (yaygın, çirkin)
/* The old hack — fragile and breaks with dynamic content */
.card-title {
min-height: 3rem; /* pray that no title exceeds this */
}
.card-body {
min-height: 8rem; /* even more prayer */
}Subgrid bu hack'i gereksiz kılar. Parent grid, her satırdaki en uzun içeriğe göre satır yüksekliklerini tüm kartlar arasında dağıtır.
Form Layout'ları İçin Sütun Subgrid#
Subgrid sütunlarda da çalışır, bu da form layout'ları için mükemmeldir:
.form {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.75rem 1rem;
}
.form-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
}
.form-row label {
grid-column: 1;
text-align: right;
font-weight: 500;
}
.form-row input {
grid-column: 2;
padding: 0.5rem;
border: 1px solid oklch(80% 0 0);
border-radius: 0.375rem;
}Tüm etiketler hizalanır. Tüm input'lar hizalanır. Etiket sütunu en geniş etikete göre otomatik boyutlanır. Sabit kodlanmış genişlik yok.
View Transitions API: Framework Olmadan SPA-Benzeri Gezinme#
Bu listede en iddialı özellik bu. View Transitions API, sayfa gezinmeleri arasında — cross-document gezinmeler (çok sayfalı bir uygulamada normal bağlantı tıklamaları) dahil — animasyon yapmanı sağlar. Statik HTML siten artık pürüzsüz, animasyonlu sayfa geçişlerine sahip olabilir.
Cross-Document Geçişler#
Cross-document view transition'larını etkinleştirmek için hem eski hem yeni sayfaya tek bir CSS kuralı eklersin:
@view-transition {
navigation: auto;
}Bu kadar. Tarayıcı artık sayfa gezinmelerinde sayfalar arasında cross-fade yapacak. Varsayılan geçiş pürüzsüz bir opacity fade'dir. JavaScript yok. Framework yok. Her sayfada iki satır CSS.
Geçişi Özelleştirme#
Belirli elementleri isimlendirerek hangi geçişlerin olacağını özelleştirebilirsin:
/* On both pages */
.page-header {
view-transition-name: header;
}
.main-content {
view-transition-name: content;
}
.hero-image {
view-transition-name: hero;
}
/* Transition animations */
::view-transition-old(content) {
animation: slide-out 0.3s ease-in both;
}
::view-transition-new(content) {
animation: slide-in 0.3s ease-out both;
}
::view-transition-old(hero) {
animation: fade-out 0.2s ease-in both;
}
::view-transition-new(hero) {
animation: none; /* The new hero just appears */
}
@keyframes slide-out {
to {
transform: translateX(-100%);
opacity: 0;
}
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}Her iki sayfada da view-transition-name: hero ile bir .hero-image olduğunda, tarayıcı otomatik olarak görseli eski sayfadaki konumundan yeni sayfadaki konumuna anime eder. Mobil geliştirmedeki "shared element transition" kalıbı, artık tarayıcıda.
SPA View Transition'ları#
Tek sayfa uygulamaları (React, Vue, Svelte vb.) için JavaScript API'si basittir:
/* CSS side — define your transition animations */
::view-transition-old(root) {
animation: fade-out 0.2s ease-in;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease-out;
}// JS side — wrap your DOM update in startViewTransition
document.startViewTransition(() => {
// Update the DOM here — React render, innerHTML swap, etc.
updateContent(newPageData);
});Tarayıcı eski durumun anlık görüntüsünü alır, güncellemeyi çalıştırır, yeni durumun anlık görüntüsünü alır ve aralarında anime eder. İsimlendirilmiş elementler ayrı geçişler alır; diğer her şey varsayılan crossfade alır.
Kullanıcı Tercihlerine Saygı Göstermek#
Her zaman prefers-reduced-motion'a saygı göster:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}Artık Kullanmayı Bırakabileceğin Şeyler#
Bu özellikler toplu olarak, yıllardır güvendiğimiz birçok aracı, kütüphaneyi ve kalıbı ortadan kaldırıyor. İşte kaldırdığım veya artık el uzatmayı bıraktığım şeyler:
Nesting ve Değişkenler İçin Sass/SCSS#
CSS'in native nesting'i var. CSS'in custom property'leri var (ve yıllardır var). İnsanların Sass'a uzanmasının iki ana nedeni artık dilin içinde. Hâlâ Sass'ı sadece $variables ve nesting için kullanıyorsan, bırakabilirsin.
/* Before: Sass */
$primary: #3b82f6;
.card {
background: white;
border: 1px solid lighten($primary, 40%);
&:hover {
border-color: $primary;
}
.title {
color: darken($primary, 15%);
}
}
/* After: Native CSS */
.card {
--primary: oklch(55% 0.2 260);
background: white;
border: 1px solid color-mix(in oklch, var(--primary) 20%, white);
&:hover {
border-color: var(--primary);
}
.title {
color: color-mix(in oklch, var(--primary) 70%, black);
}
}CSS versiyonu daha güçlü — oklch algısal olarak tek tip renk manipülasyonu sağlar, color-mix çalışma zamanında çalışır (Sass lighten sadece derleme zamanı), ve custom property'ler JavaScript'ten veya media query'lerden dinamik olarak değiştirilebilir.
Scroll Animasyonları İçin JavaScript#
IntersectionObserver reveal-on-scroll kodunu sil. Scroll ilerleme çubuğu JavaScript'ini sil. Parallax scroll handler'ını sil. animation-timeline: scroll() ve animation-timeline: view() bunların hepsini daha iyi performansla (compositor thread, main thread değil) halleder.
// Before: JavaScript you can delete
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));/* After: CSS that replaces all of the above */
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}Konumlandırma İçin Popper.js / Floating UI#
Floating UI'ı (eski adıyla Popper.js) sadece temel tooltip/popover konumlandırması için kullanıyorsan, CSS anchor positioning onun yerini alır. Bazı gelişmiş özellikleri (sanal elementler, özel middleware) kaybedersin, ama %90 kullanım durumu olan "bu popover'ı şu butonun yanına koy ve viewport içinde tut" için CSS bunu artık yerel olarak yapıyor.
Bileşen Düzeyinde Duyarlılık İçin Media Query'ler#
Bu büyük olanı. Bir bileşeni duyarlı yapmak için @media query'ler yazıyorsan, muhtemelen artık yanlış yapıyorsun. Container query'ler bileşen düzeyinde duyarlılık için varsayılanın olmalı. @media query'leri sayfa düzeyinde kararlar için ayır: layout değişiklikleri, gezinme kalıpları, baskı stilleri.
/* Before: Media query for component styling (wrong scope) */
@media (min-width: 768px) {
.product-card {
flex-direction: row;
}
}
/* After: Container query (right scope) */
.product-card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.product-card {
flex-direction: row;
}
}Büyük Resim#
Bu özellikler izole olarak var olmuyor. Bileşiyor. Container query + :has() + nesting + oklch() + layer — birlikte kullanıldığında — beş yıl önce tanınmaz olacak bir CSS yazma deneyimi veriyor:
@layer components {
.card-wrapper {
container-type: inline-size;
}
.card {
--accent: oklch(55% 0.2 260);
display: grid;
gap: 1rem;
padding: 1.5rem;
border-radius: 0.5rem;
background: white;
border: 1px solid oklch(90% 0 0);
&:has(img) {
padding-top: 0;
img {
border-radius: 0.5rem 0.5rem 0 0;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
}
&:has(.badge.new) {
border-color: var(--accent);
}
.title {
font-size: 1.125rem;
font-weight: 600;
color: oklch(20% 0 0);
text-wrap: balance;
}
.meta {
font-size: 0.875rem;
color: oklch(50% 0 0);
}
@container (min-width: 500px) {
grid-template-columns: 200px 1fr;
&:has(img) {
padding: 0;
img {
border-radius: 0.5rem 0 0 0.5rem;
height: 100%;
aspect-ratio: auto;
}
}
.content {
padding: 1.5rem;
}
}
}
}Bu tek blok şunları halleder:
- Bileşen düzeyinde duyarlı layout (container query)
- İçeriğe göre koşullu stilleme (
:has()) - Temiz seçici organizasyonu (nesting)
- Algısal olarak tek tip renkler (
oklch) - Öngörülebilir specificity yönetimi (
@layer)
Ön işlemci yok. CSS-in-JS runtime yok. Bunun için utility sınıfları yok. Sadece CSS, CSS'in başından beri yapması gereken şeyi yapıyor.
Platform nihayet yetişti. Ve beklemeye değdi.