CSS Modern di 2026: Fitur yang Mengubah Cara Saya Menulis Styles
Container queries, CSS layers, :has(), color-mix(), nesting, scroll-driven animations, dan anchor positioning. Fitur CSS yang membuat saya berhenti bergantung pada JavaScript.
Saya menghapus file Sass terakhir saya enam bulan lalu. Bukan karena saya ingin membuat pernyataan. Karena saya benar-benar tidak membutuhkannya lagi.
Selama lebih dari satu dekade, CSS adalah bahasa yang selalu kita mintakan maaf. Kita membutuhkan preprocessor untuk nesting dan variabel. Kita membutuhkan JavaScript untuk ukuran berbasis container, animasi yang terhubung dengan scroll, seleksi parent, dan separuh pola layout yang diserahkan desainer kepada kita. Kita membangun seluruh sistem runtime — library CSS-in-JS, framework utilitas, rantai plugin PostCSS — untuk mengkompensasi apa yang tidak bisa dilakukan bahasa ini secara native.
Era itu sudah berakhir. Bukan "hampir berakhir." Bukan "sedang menuju ke sana." Berakhir.
Fitur-fitur yang mendarat di browser antara 2024 dan 2026 tidak hanya menambah kenyamanan. Mereka mengubah model mental. CSS bukan lagi bahasa styling yang harus kamu lawan. Ini adalah bahasa styling yang benar-benar memikirkan komponen, manajemen specificity, desain responsif di level elemen, dan animasi tanpa runtime.
Berikut apa yang berubah, mengapa itu penting, dan apa yang bisa kamu berhenti lakukan karenanya.
Container Queries: Akhir dari "Berapa Lebar Viewport?"#
Ini adalah fitur yang secara fundamental mengubah cara saya berpikir tentang desain responsif. Selama dua puluh tahun, kita punya media queries. Media queries bertanya: "berapa lebar viewport?" Container queries bertanya: "berapa lebar container tempat komponen ini berada?"
Perbedaan itu terdengar halus. Tidak. Ini perbedaan antara komponen yang hanya bekerja dalam satu konteks layout dan komponen yang bekerja di mana saja.
Masalah yang Ada Selama Dua Dekade#
Bayangkan komponen card. Di sidebar, ia harus ditumpuk secara vertikal dengan gambar kecil. Di area konten utama, ia harus horizontal dengan gambar lebih besar. Di bagian hero full-width, ia harus menjadi sesuatu yang berbeda lagi.
Dengan media queries, kamu akan menulis sesuatu seperti ini:
/* Cara lama: menggabungkan style komponen ke layout halaman */
.card {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.sidebar .card {
/* Masih vertikal di sidebar */
}
.main-content .card {
flex-direction: row;
/* Horizontal di area utama */
}
}
@media (min-width: 1200px) {
.hero .card {
/* Layout lain lagi */
}
}Card tahu tentang .sidebar, .main-content, dan .hero. Ia tahu tentang halaman. Ia bukan komponen lagi — ia adalah fragmen yang sadar halaman. Pindahkan ke halaman lain dan semuanya rusak.
Container Queries Memperbaiki Ini Sepenuhnya#
/* Cara container query: komponen hanya tahu tentang dirinya sendiri */
.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;
}
}Card tidak tahu di mana ia tinggal. Ia tidak peduli. Taruh di sidebar 300px dan ia vertikal. Taruh di area utama 700px dan ia horizontal. Jatuhkan di bagian full-width dan ia menyesuaikan. Nol pengetahuan tentang layout halaman yang dibutuhkan.
inline-size vs size#
Kamu hampir selalu ingin container-type: inline-size. Ini mengaktifkan query pada sumbu inline (lebar di mode penulisan horizontal). Menggunakan container-type: size mengaktifkan query sumbu inline dan block, tapi memerlukan container memiliki ukuran eksplisit di kedua dimensi, yang merusak alur dokumen normal di kebanyakan kasus.
/* Ini yang kamu inginkan 99% dari waktu */
.wrapper {
container-type: inline-size;
}
/* Ini memerlukan tinggi eksplisit — jarang yang kamu inginkan */
.wrapper-both {
container-type: size;
height: 500px; /* diperlukan, atau ia akan collapse */
}Container Bernama untuk Konteks Bersarang#
Saat kamu menyarangkan container, penamaan menjadi penting:
.page-layout {
container-type: inline-size;
container-name: layout;
}
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
/* Targetkan sidebar secara spesifik, bukan ancestor terdekat */
@container sidebar (max-width: 250px) {
.nav-item {
font-size: 0.875rem;
padding: 0.25rem;
}
}
/* Targetkan layout halaman */
@container layout (min-width: 1200px) {
.page-header {
font-size: 2.5rem;
}
}Tanpa nama, @container meng-query ancestor terdekat yang memiliki containment. Dengan nesting, itu sering bukan container yang kamu inginkan. Beri nama. Selalu.
Unit Container Query#
Yang satu ini kurang dihargai. Unit container query (cqi, cqb, cqw, cqh) memungkinkanmu mengukur sesuatu relatif terhadap container, bukan viewport:
@container (min-width: 400px) {
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
}4cqi adalah 4% dari ukuran inline container. Judul mengikuti skala container, bukan jendela. Inilah yang seharusnya menjadi tipografi fluid sejak awal.
CSS Layers: Perang Specificity Sudah Berakhir#
Jika container queries mengubah cara saya berpikir tentang desain responsif, @layer mengubah cara saya berpikir tentang arsitektur CSS. Untuk pertama kalinya, kita memiliki cara yang waras dan deklaratif untuk mengelola specificity di seluruh proyek.
Masalahnya#
Specificity CSS adalah sistem poin yang tidak peduli dengan niatmu. Kelas utilitas dengan .text-red kalah dari .card .title karena yang terakhir memiliki specificity lebih tinggi. Perbaikannya selalu sama: buat selectormu lebih spesifik, tambahkan !important, atau restrukturisasi semuanya.
Kita membangun seluruh metodologi (BEM, SMACSS, ITCSS) dan toolchain hanya untuk menghindari konflik specificity. Semua itu adalah workaround untuk fitur bahasa yang hilang.
Urutan Layer#
@layer memungkinkanmu mendeklarasikan urutan di mana kelompok style dipertimbangkan, terlepas dari specificity di dalam kelompok tersebut:
/* Deklarasikan urutan layer — satu baris ini mengontrol semuanya */
@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);
}
}Meskipun .card .title memiliki specificity lebih tinggi dari .text-red, utilitas menang karena layer utilities dideklarasikan setelah components. Tanpa !important. Tanpa hack specificity. Urutan layer adalah kata terakhir.
Bagaimana Tailwind CSS v4 Menggunakan Layers#
Tailwind v4 sangat bergantung pada @layer. Saat kamu menulis @import "tailwindcss", kamu mendapatkan:
@layer theme, base, components, utilities;Setiap utilitas Tailwind berada di layer utilities. Style komponen kustommu masuk ke components. Inilah mengapa kelas text-red-500 bisa meng-override warna komponen tanpa perlu !important — ia berada di layer yang lebih belakang.
Jika kamu membangun design system sendiri tanpa Tailwind, curi arsitektur ini. Ini yang benar:
@layer reset, tokens, base, layouts, components, utilities, overrides;Tujuh layer sudah cukup. Saya tidak pernah membutuhkan lebih.
Style Tanpa Layer Mengalahkan Semuanya#
Satu jebakan: style yang tidak ada di layer mana pun memiliki prioritas tertinggi. Ini sebenarnya berguna — artinya override spesifik halaman satu kali langsung menang:
@layer components {
.modal {
background: white;
}
}
/* Tidak ada di layer mana pun — menang atas semuanya di dalam layer */
.special-page .modal {
background: oklch(95% 0.02 260);
}Tapi ini juga berarti CSS pihak ketiga yang tidak mengenal layer bisa meng-override seluruh sistemmu. Bungkus style pihak ketiga dalam layer untuk mengontrolnya:
@layer third-party {
@import url("some-library.css");
}Selector :has(): Parent Selector yang Selalu Kita Inginkan#
Selama benar-benar berpuluh-puluh tahun, developer meminta parent selector. "Saya ingin men-style parent berdasarkan childrennya." Jawabannya selalu "CSS tidak bisa melakukan itu" diikuti oleh workaround JavaScript. :has() mengubah ini sepenuhnya, dan ternyata ia bahkan lebih powerful dari yang kita minta.
Seleksi Parent Dasar#
/* Style form group saat inputnya mendapat fokus */
.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 card berbeda saat mengandung gambar */
.card:has(img) {
padding-top: 0;
}
.card:has(img) .card-content {
padding: 1.5rem;
}
/* Card tanpa gambar mendapat perlakuan berbeda */
.card:not(:has(img)) {
border-left: 4px solid oklch(55% 0.2 260);
}State Validasi Form Tanpa JavaScript#
Di sinilah :has() menjadi benar-benar menarik. Dikombinasikan dengan pseudo-class validasi HTML, kamu bisa membangun UI form yang merespons state validitas tanpa JavaScript sama sekali:
/* Wrapper field bereaksi terhadap validitas inputnya */
.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;
}Bagian :not(:placeholder-shown) sangat penting — ia mencegah style validasi muncul di field kosong yang belum disentuh.
Layout Berdasarkan Jumlah Children#
Pola ini sangat berguna dan benar-benar mustahil sebelum :has():
/* Sesuaikan kolom grid berdasarkan berapa banyak item yang ada */
.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 mengubah jumlah kolomnya berdasarkan berapa banyak children yang dimilikinya. Tanpa JavaScript. Tanpa ResizeObserver. Tanpa toggling class.
Deteksi Sidebar#
Salah satu pola favorit saya — mengubah layout berdasarkan apakah sidebar ada:
.page-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
/* Jika layout mengandung sidebar, beralih ke dua kolom */
.page-layout:has(.sidebar) {
grid-template-columns: 1fr 300px;
}
/* Sesuaikan lebar konten utama saat sidebar ada */
.page-layout:has(.sidebar) .main-content {
max-width: 65ch;
}Tambahkan komponen sidebar ke DOM, layout menyesuaikan. Hapus, ia menyesuaikan kembali. CSS adalah sumber kebenaran, bukan variabel state.
Mengkombinasikan :has() dengan Selector Lain#
:has() bisa dikombinasikan dengan indah bersama yang lain:
/* Style artikel hanya saat memiliki kelas figure tertentu */
article:has(figure.full-bleed) {
overflow: visible;
}
/* Navigasi yang berubah saat mengandung input pencarian yang fokus */
nav:has(input[type="search"]:focus) {
background: oklch(98% 0 0);
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.08);
}
/* Aktifkan dark-mode di level komponen */
.theme-switch:has(input:checked) ~ main {
color-scheme: dark;
background: oklch(15% 0 0);
color: oklch(90% 0 0);
}Dukungan browser sudah sangat baik sekarang. Setiap browser modern telah mendukung :has() sejak awal 2024. Tidak ada alasan untuk tidak menggunakannya.
CSS Nesting: Native, Akhirnya Stabil#
Saya tidak akan melebih-lebihkan yang satu ini. CSS nesting itu bagus. Ia tidak revolusioner seperti container queries atau :has(). Tapi ia menghilangkan salah satu alasan terakhir untuk menggunakan Sass, dan itu penting.
Sintaksnya#
.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);
}
}Ini adalah CSS yang valid. Tanpa build step. Tanpa preprocessor. Tanpa plugin PostCSS. Browser menanganinya secara native.
Perbedaan dari Sass#
Ada beberapa perbedaan sintaks yang perlu diketahui:
/* CSS Nesting — sudah berfungsi di semua browser */
.parent {
/* Nesting class/elemen langsung berfungsi tanpa & */
.child {
color: red;
}
/* & diperlukan untuk pseudo-class dan compound selector */
&:hover {
background: blue;
}
&.active {
font-weight: bold;
}
/* Media queries bersarang — ini keren */
@media (width >= 768px) {
flex-direction: row;
}
}Di implementasi awal, kamu perlu & sebelum selector elemen (seperti & p bukannya hanya p). Pembatasan itu sudah dilonggarkan. Sejak 2025, semua browser utama mendukung bare element nesting: .parent { p { ... } } berfungsi dengan baik.
Media Queries Bersarang#
Ini adalah fitur killer dari CSS nesting, menurut pendapat saya. Bukan nesting selectornya — kemampuan untuk meletakkan media queries di dalam blok aturan:
.hero {
padding: 2rem;
font-size: 1rem;
@media (width >= 768px) {
padding: 4rem;
font-size: 1.25rem;
}
@media (width >= 1200px) {
padding: 6rem;
font-size: 1.5rem;
}
}Bandingkan ini dengan cara lama di mana style .hero tersebar di tiga blok @media yang berbeda, mungkin terpisah ratusan baris. Nesting menjaga perilaku responsif tetap di tempat yang sama dengan komponen. Keterbacaan meningkat secara dramatis.
Jangan Terlalu Dalam Nesting#
Satu peringatan: hanya karena kamu bisa menyarangkan enam level tidak berarti kamu harus. Saran yang sama dari Sass berlaku di sini. Jika nesting-mu membuat selector seperti .page .section .card .content .text .highlight, kamu telah membuat monster specificity dan mimpi buruk pemeliharaan. Dua atau tiga level adalah titik manisnya.
/* Bagus — dua level */
.nav {
.link {
color: inherit;
&:hover {
color: oklch(55% 0.2 260);
}
}
}
/* Buruk — mimpi buruk specificity */
.page {
.layout {
.sidebar {
.nav {
.list {
.item {
.link {
color: red; /* semoga berhasil meng-override ini */
}
}
}
}
}
}
}Fungsi Warna: oklch() dan color-mix() Mengubah Segalanya#
hsl() telah berjalan dengan baik. Ia lebih intuitif dari rgb(). Tapi ia memiliki cacat fundamental: ia tidak seragam secara perseptual. hsl(60, 100%, 50%) (kuning) terlihat jauh lebih terang bagi mata manusia dibanding hsl(240, 100%, 50%) (biru), meskipun keduanya memiliki nilai lightness yang sama.
Mengapa oklch() Menang#
oklch() seragam secara perseptual. Nilai lightness yang sama terlihat sama terangnya. Ini sangat penting saat menghasilkan palet warna, membuat tema, dan memastikan kontras yang aksesibel:
:root {
/* oklch(lightness chroma hue) */
--color-primary: oklch(55% 0.2 260); /* biru */
--color-secondary: oklch(55% 0.2 330); /* ungu */
--color-success: oklch(55% 0.2 145); /* hijau */
--color-danger: oklch(55% 0.25 25); /* merah */
--color-warning: oklch(75% 0.18 85); /* kuning — L lebih tinggi untuk persepsi yang sama */
/* Semua ini terlihat sama "sedang" bagi mata manusia */
/* Dengan HSL, kamu perlu nilai lightness berbeda untuk setiap hue */
}Tiga nilainya intuitif begitu kamu memahaminya:
- Lightness (0% hingga 100%): Seberapa terang atau gelap. 0% adalah hitam, 100% adalah putih.
- Chroma (0 hingga ~0.37): Seberapa vivid. 0 adalah abu-abu, lebih tinggi lebih jenuh.
- Hue (0 hingga 360): Sudut warna. 0/360 adalah merah kemerahan, 145 adalah hijau, 260 adalah biru.
color-mix() untuk Warna Turunan#
color-mix() memungkinkanmu membuat warna dari warna lain saat runtime. Tanpa fungsi darken() Sass. Tanpa JavaScript. Hanya CSS:
:root {
--brand: oklch(55% 0.2 260);
/* Terangkan dengan mencampur dengan putih */
--brand-light: color-mix(in oklch, var(--brand) 30%, white);
/* Gelapkan dengan mencampur dengan hitam */
--brand-dark: color-mix(in oklch, var(--brand) 70%, black);
/* Buat latar belakang halus */
--brand-bg: color-mix(in oklch, var(--brand) 8%, white);
/* Versi semi-transparan */
--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);
}
}Bagian in oklch itu penting. Mencampur di srgb menghasilkan warna antara yang keruh. Mencampur di oklch menghasilkan hasil yang merata secara perseptual. Selalu campur di oklch.
Membangun Palet Lengkap dengan oklch()#
Berikut cara saya menghasilkan seluruh palet shade dari satu hue:
: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));
}Ubah --hue ke 145 dan kamu mendapatkan palet hijau. Ubah ke 25 dan kamu mendapatkan merah. Langkah lightness-nya seragam secara perseptual. Chroma meruncing di ujung-ujung sehingga shade paling terang dan gelap tidak terlalu jenuh. Ini adalah jenis hal yang dulu memerlukan alat desain atau fungsi Sass. Sekarang delapan baris CSS.
Scroll-Driven Animations: Tanpa JavaScript#
Ini adalah fitur yang membuat saya menghapus paling banyak JavaScript. Animasi yang terhubung dengan scroll — progress bar, efek parallax, animasi reveal, header sticky dengan transisi — dulu memerlukan IntersectionObserver, event listener scroll, atau library seperti GSAP. Sekarang ini CSS.
Indikator Progres Scroll#
Progress bar "membaca" klasik di bagian atas halaman artikel:
.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);
}
}Itu saja. Seluruh indikator progres membaca. Tanpa JavaScript. Tanpa event listener scroll. Tanpa requestAnimationFrame. Tanpa kalkulasi "persentase scroll." Binding animation-timeline: scroll() melakukan semuanya.
Reveal saat Scroll#
Elemen yang fade in saat memasuki viewport:
.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() mengikat animasi ke visibilitas elemen di viewport. animation-range: entry 0% entry 100% berarti animasi berjalan dari saat elemen mulai memasuki viewport hingga ia sepenuhnya terlihat.
Parallax Tanpa Library#
.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;
}Gambar latar bergerak dengan kecepatan berbeda dari scroll, menciptakan efek parallax. Halus, performant (browser bisa melakukan compositing di GPU), dan tanpa JavaScript sama sekali.
Named Scroll Timelines#
Untuk kontrol lebih, kamu bisa menamai scroll timeline dan mereferensikannya dari elemen lain:
.scroller {
overflow-y: auto;
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: block;
}
/* Elemen di mana pun di DOM bisa mereferensikan timeline ini */
.indicator {
animation: progress linear both;
animation-timeline: --my-scroller;
}
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}Ini bekerja bahkan saat indikator dan scroller bukan parent/child. Elemen mana pun bisa terhubung ke named scroll timeline mana pun. Ini powerful untuk UI dashboard di mana panel yang bisa di-scroll menggerakkan indikator di header yang fixed.
Anchor Positioning: Tooltip dan Popover yang Benar#
Sebelum anchor positioning, menghubungkan tooltip ke elemen trigger-nya memerlukan JavaScript. Kamu menghitung posisi dengan getBoundingClientRect(), menangani offset scroll, mengelola tabrakan viewport, dan menghitung ulang saat resize. Library seperti Popper.js (sekarang Floating UI) ada secara khusus karena ini sangat sulit dilakukan dengan benar.
CSS anchor positioning membuat ini deklaratif:
.trigger {
anchor-name: --my-trigger;
}
.tooltip {
position: fixed;
position-anchor: --my-trigger;
/* Posisikan center-atas tooltip di center-bawah trigger */
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
/* Positioning fallback jika overflow viewport */
position-try-fallbacks: flip-block, flip-inline;
}Penanganan Tabrakan Viewport Otomatis#
Properti position-try-fallbacks adalah bagian yang memerlukan 200 baris JavaScript. Ia memberi tahu browser: "jika tooltip overflow di bawah viewport, balik ke atas. Jika overflow di kanan, balik ke kiri."
.dropdown-menu {
position: fixed;
position-anchor: --menu-button;
/* Default: di bawah tombol, sejajar dengan tepi kiri */
top: anchor(bottom);
left: anchor(left);
/* Jika tidak muat di bawah, coba di atas. Jika tidak muat rata kiri, coba rata kanan */
position-try-fallbacks: flip-block, flip-inline;
/* Tambahkan jarak antara anchor dan dropdown */
margin-top: 4px;
}Named Position Fallback#
Untuk kontrol lebih atas posisi fallback, kamu bisa mendefinisikan opsi try kustom:
@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;
}Browser mencoba setiap fallback secara berurutan sampai menemukan yang menjaga elemen tetap dalam viewport. Ini adalah jenis penalaran spasial yang benar-benar menyakitkan di JavaScript.
Anchoring dengan Popover API#
Anchor positioning berpasangan sempurna dengan Popover API baru:
<button popovertarget="my-popover" style="anchor-name: --btn">Pengaturan</button>
<div popover id="my-popover" style="
position-anchor: --btn;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
margin-top: 8px;
">
Konten popover di sini
</div>Tanpa JavaScript untuk menampilkan/menyembunyikan (Popover API menangani itu). Tanpa JavaScript untuk positioning (anchor positioning menangani itu). Tanpa JavaScript untuk perilaku light-dismiss (Popover API juga menangani itu). Seluruh pola tooltip/popover/dropdown — pola yang memicu seluruh paket npm — sekarang adalah HTML dan CSS.
CSS Grid Subgrid: Penyejajaran Bersarang yang Benar-Benar Bekerja#
Grid itu powerful, tapi memiliki satu keterbatasan yang membuat frustrasi: child grid tidak bisa menyejajarkan item-nya ke parent grid. Jika kamu memiliki deretan card dan ingin judul, konten, dan footer setiap card sejajar antar card, kamu kurang beruntung. Grid internal setiap card bersifat independen.
Subgrid memperbaiki ini.
Masalah Penyejajaran Card#
/* Parent grid */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
/* Setiap card menjadi subgrid, menyejajarkan baris ke parent */
.card {
display: grid;
grid-row: span 3; /* Card menempati 3 baris di parent */
grid-template-rows: subgrid;
gap: 0; /* Card mengontrol gap internalnya sendiri */
}
.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);
}Sekarang judul di ketiga card sejajar di baris yang sama. Body-nya sejajar. Footer-nya sejajar. Bahkan saat satu card memiliki judul dua baris dan card lain memiliki judul satu baris, penyejajaran dipertahankan oleh parent grid.
Sebelum Subgrid: Hack-nya#
Tanpa subgrid, mencapai penyejajaran ini memerlukan:
- Tinggi tetap (rapuh, rusak dengan konten dinamis)
- Pengukuran JavaScript (lambat, flash of misalignment)
- Menyerah dan menerima ketidaksejajaran (umum, jelek)
/* Hack lama — rapuh dan rusak dengan konten dinamis */
.card-title {
min-height: 3rem; /* berdoa agar tidak ada judul yang melebihi ini */
}
.card-body {
min-height: 8rem; /* doa lebih banyak lagi */
}Subgrid membuat hack itu tidak diperlukan. Parent grid mendistribusikan tinggi baris berdasarkan konten tertinggi di setiap baris di semua card.
Column Subgrid untuk Layout Form#
Subgrid juga berfungsi untuk kolom, yang sempurna untuk layout form:
.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;
}Semua label sejajar. Semua input sejajar. Kolom label menyesuaikan ukuran secara otomatis ke label terlebar. Tanpa lebar yang di-hardcode.
View Transitions API: Navigasi Mirip SPA Tanpa Framework#
Yang satu ini adalah fitur paling ambisius di daftar ini. View Transitions API memungkinkanmu menganimasikan antara navigasi halaman — termasuk navigasi cross-document (klik link biasa di aplikasi multi-halaman). Situs HTML statismu sekarang bisa memiliki transisi halaman yang halus dan beranimasi.
Transisi Cross-Document#
Untuk mengaktifkan view transition cross-document, kamu menambahkan satu aturan CSS ke halaman lama dan baru:
@view-transition {
navigation: auto;
}Itu saja. Browser sekarang akan melakukan cross-fade antara halaman saat navigasi. Transisi default adalah fade opacity yang halus. Tanpa JavaScript. Tanpa framework. Hanya dua baris CSS di setiap halaman.
Menyesuaikan Transisi#
Kamu bisa menyesuaikan transisi apa yang terjadi dengan menamai elemen tertentu:
/* Di kedua halaman */
.page-header {
view-transition-name: header;
}
.main-content {
view-transition-name: content;
}
.hero-image {
view-transition-name: hero;
}
/* Animasi transisi */
::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; /* Hero baru langsung muncul */
}
@keyframes slide-out {
to {
transform: translateX(-100%);
opacity: 0;
}
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}Saat bernavigasi antara dua halaman yang keduanya memiliki .hero-image dengan view-transition-name: hero, browser otomatis menganimasikan gambar dari posisinya di halaman lama ke posisinya di halaman baru. Ini adalah pola "shared element transition" dari pengembangan mobile, sekarang di browser.
SPA View Transitions#
Untuk single-page app (React, Vue, Svelte, dll.), JavaScript API-nya langsung:
/* Sisi CSS — definisikan animasi transisi */
::view-transition-old(root) {
animation: fade-out 0.2s ease-in;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease-out;
}// Sisi JS — bungkus update DOM dalam startViewTransition
document.startViewTransition(() => {
// Update DOM di sini — React render, innerHTML swap, dll.
updateContent(newPageData);
});Browser mengambil snapshot state lama, menjalankan update-mu, mengambil snapshot state baru, dan menganimasikan di antaranya. Elemen bernama mendapat transisi individual; yang lainnya mendapat crossfade default.
Menghormati Preferensi Pengguna#
Selalu hormati prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}Apa yang Bisa Kamu Berhenti Gunakan#
Fitur-fitur ini secara kolektif mengeliminasi banyak tooling, library, dan pola yang telah kita andalkan selama bertahun-tahun. Berikut apa yang sudah saya hapus atau berhenti gunakan:
Sass/SCSS untuk Nesting dan Variabel#
CSS memiliki nesting native. CSS memiliki custom properties (dan sudah memilikinya selama bertahun-tahun). Dua alasan utama orang menggunakan Sass sekarang ada di bahasanya. Jika kamu masih menggunakan Sass hanya untuk $variables dan nesting, kamu bisa berhenti.
/* Sebelumnya: Sass */
$primary: #3b82f6;
.card {
background: white;
border: 1px solid lighten($primary, 40%);
&:hover {
border-color: $primary;
}
.title {
color: darken($primary, 15%);
}
}
/* Sesudahnya: CSS Native */
.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);
}
}Versi CSS lebih powerful — oklch memberikan manipulasi warna yang seragam secara perseptual, color-mix bekerja saat runtime (Sass lighten hanya compile-time), dan custom properties bisa diubah secara dinamis dari JavaScript atau media queries.
JavaScript untuk Animasi Scroll#
Hapus kode IntersectionObserver reveal-on-scroll-mu. Hapus JavaScript progress bar scroll-mu. Hapus handler scroll parallax-mu. animation-timeline: scroll() dan animation-timeline: view() menangani semua ini dengan performa lebih baik (compositor thread, bukan main thread).
// Sebelumnya: JavaScript yang bisa kamu hapus
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));/* Sesudahnya: CSS yang menggantikan semua di atas */
.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); }
}Popper.js / Floating UI untuk Positioning#
Jika kamu menggunakan Floating UI (dulunya Popper.js) hanya untuk positioning tooltip/popover dasar, CSS anchor positioning menggantikannya. Kamu kehilangan beberapa fitur lanjutan (virtual elements, custom middleware), tapi untuk 90% kasus penggunaan — "taruh popover ini dekat tombol itu dan jaga tetap di viewport" — CSS melakukannya secara native sekarang.
Hack Centering margin: auto#
Ini bukan baru, tapi saya masih melihat margin: 0 auto di mana-mana. Alat layout modern membuatnya tidak diperlukan di kebanyakan kasus:
/* Cara lama */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
/* Lebih baik: CSS Grid atau Flexbox */
.page {
display: grid;
place-items: center;
}
/* Atau: container queries membuat container itu sendiri responsif */
.content {
container-type: inline-size;
width: min(100% - 2rem, 1200px);
margin-inline: auto; /* OK, masih auto, tapi margin-inline lebih jelas */
}margin: 0 auto masih berfungsi dengan baik. Tapi jika kamu menggunakannya untuk centering vertikal atau alignment kompleks, gunakan flexbox atau grid saja.
Media Queries untuk Responsivitas Level Komponen#
Ini yang besar. Jika kamu menulis @media queries untuk membuat komponen responsif, kamu kemungkinan melakukannya dengan salah sekarang. Container queries seharusnya menjadi default untuk responsivitas level komponen. Cadangkan @media queries untuk keputusan level halaman: perubahan layout, pola navigasi, style cetak.
/* Sebelumnya: Media query untuk styling komponen (scope salah) */
@media (min-width: 768px) {
.product-card {
flex-direction: row;
}
}
/* Sesudahnya: Container query (scope benar) */
.product-card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.product-card {
flex-direction: row;
}
}Gambaran Besar#
Fitur-fitur ini tidak ada secara terpisah. Mereka saling melengkapi. Container queries + :has() + nesting + oklch() + layers — digunakan bersama — memberikan pengalaman menulis CSS yang tidak bisa dikenali lima tahun lalu:
@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;
}
}
}
}Blok tunggal itu menangani:
- Layout responsif level komponen (container queries)
- Styling kondisional berdasarkan konten (
:has()) - Organisasi selector yang bersih (nesting)
- Warna yang seragam secara perseptual (
oklch) - Manajemen specificity yang dapat diprediksi (
@layer)
Tanpa preprocessor. Tanpa runtime CSS-in-JS. Tanpa kelas utilitas untuk ini. Hanya CSS, melakukan apa yang seharusnya CSS lakukan sejak dulu.
Platform akhirnya mengejar ketertinggalan. Dan penantiannya sepadan.