सामग्री पर जाएं
·24 मिनट पढ़ने का समय

2026 में Modern CSS: वो Features जिन्होंने मेरा Styles लिखने का तरीका बदल दिया

Container queries, CSS layers, :has(), color-mix(), nesting, scroll-driven animations, और anchor positioning। वो CSS features जिन्होंने मुझे JavaScript के लिए हाथ बढ़ाना बंद करवा दिया।

साझा करें:X / TwitterLinkedIn

मैंने अपनी आखिरी Sass file छह महीने पहले delete की। कोई बयान देने के लिए नहीं। क्योंकि मुझे वास्तव में इसकी ज़रूरत नहीं रही।

एक दशक से ज़्यादा तक, CSS वो भाषा थी जिसके लिए हम माफ़ी मांगते थे। Nesting और variables के लिए preprocessors चाहिए थे। Container-based sizing, scroll-linked animations, parent selection, और आधे layout patterns के लिए JavaScript चाहिए था जो designers हमें देते थे। हमने पूरे runtime systems बनाए — CSS-in-JS libraries, utility frameworks, PostCSS plugin chains — उसकी भरपाई करने के लिए जो भाषा natively नहीं कर सकती थी।

वो दौर खत्म हो गया। "लगभग खत्म" नहीं। "वहां पहुंच रहे हैं" नहीं। खत्म।

2024 और 2026 के बीच browsers में जो features आए, उन्होंने सिर्फ़ सुविधा नहीं जोड़ी। उन्होंने mental model बदल दिया। CSS अब ऐसी styling language नहीं है जिसके खिलाफ़ लड़ना पड़े। यह ऐसी styling language है जो वास्तव में components, specificity management, element-level responsive design, और बिना runtime के animation के बारे में सोचती है।

यहां बताता हूं क्या बदला, क्यों मायने रखता है, और इसकी वजह से क्या करना बंद कर सकते हैं।

Container Queries: "Viewport कितना चौड़ा है?" का अंत#

यह वो feature है जिसने मूल रूप से बदल दिया कि मैं responsive design के बारे में कैसे सोचता हूं। बीस साल से, हमारे पास media queries थीं। Media queries पूछती हैं: "viewport कितना चौड़ा है?" Container queries पूछती हैं: "यह component जिस container में रहता है वो कितना चौड़ा है?"

यह अंतर सूक्ष्म लगता है। नहीं है। यह उन components के बीच का फ़र्क है जो सिर्फ़ एक layout context में काम करते हैं और जो हर जगह काम करते हैं।

दो दशकों से मौजूद समस्या#

एक card component सोचिए। Sidebar में, इसे छोटी image के साथ vertically stack होना चाहिए। Main content area में, बड़ी image के साथ horizontal जाना चाहिए। Full-width hero section में, कुछ और ही होना चाहिए।

Media queries से, आप कुछ ऐसा लिखते:

css
/* पुराना तरीका: component styles को page layout से जोड़ना */
.card {
  display: flex;
  flex-direction: column;
}
 
@media (min-width: 768px) {
  .sidebar .card {
    /* Sidebar में अभी भी vertical */
  }
 
  .main-content .card {
    flex-direction: row;
    /* Main area में horizontal */
  }
}
 
@media (min-width: 1200px) {
  .hero .card {
    /* फिर एक और layout */
  }
}

Card को .sidebar, .main-content, और .hero के बारे में पता है। इसे page का पता है। यह अब component नहीं रहा — यह page-aware fragment है। इसे दूसरे page पर ले जाइए और सब टूट जाता है।

Container Queries इसे पूरी तरह ठीक करती हैं#

css
/* Container query तरीका: component सिर्फ़ खुद के बारे में जानता है */
.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 को नहीं पता कहां रहता है। इसे फ़र्क नहीं पड़ता। 300px sidebar में रखो और vertical है। 700px main area में रखो और horizontal। Full-width section में डालो और adapt हो जाता है। Page layout का शून्य ज्ञान ज़रूरी।

inline-size बनाम size#

आप लगभग हमेशा container-type: inline-size चाहेंगे। यह inline axis (horizontal writing modes में width) पर queries enable करता है। container-type: size inline और block दोनों axis queries enable करता है, लेकिन container को दोनों dimensions में explicit sizing चाहिए, जो ज़्यादातर cases में normal document flow तोड़ता है।

css
/* 99% समय यही चाहिए */
.wrapper {
  container-type: inline-size;
}
 
/* इसमें explicit height ज़रूरी — शायद ही कभी चाहते हैं */
.wrapper-both {
  container-type: size;
  height: 500px; /* ज़रूरी, वरना collapse हो जाएगा */
}

Nested Contexts के लिए Named Containers#

जब containers nest करें, naming ज़रूरी हो जाती है:

css
.page-layout {
  container-type: inline-size;
  container-name: layout;
}
 
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}
 
/* Specifically sidebar target करें, nearest ancestor नहीं */
@container sidebar (max-width: 250px) {
  .nav-item {
    font-size: 0.875rem;
    padding: 0.25rem;
  }
}
 
/* Page layout target करें */
@container layout (min-width: 1200px) {
  .page-header {
    font-size: 2.5rem;
  }
}

Names के बिना, @container nearest ancestor को query करता है जिसमें containment है। Nesting के साथ, अक्सर वो container नहीं होता जो चाहिए। नाम दीजिए। हमेशा।

Container Query Units#

यह underrated है। Container query units (cqi, cqb, cqw, cqh) चीज़ों को viewport नहीं, container के relative size करने देते हैं:

css
@container (min-width: 400px) {
  .card-title {
    font-size: clamp(1rem, 4cqi, 2rem);
  }
}

4cqi container की inline size का 4% है। Title container के साथ scale करता है, window के साथ नहीं। Fluid typography को शुरू से ऐसा ही होना चाहिए था।

CSS Layers: Specificity War खत्म#

अगर container queries ने बदला कि मैं responsive design के बारे में कैसे सोचता हूं, तो @layer ने बदला कि मैं CSS architecture के बारे में कैसे सोचता हूं। पहली बार, हमारे पास पूरे project में specificity manage करने का एक समझदार, declarative तरीका है।

समस्या#

CSS specificity एक point system है जिसे आपकी intentions से कोई मतलब नहीं। .text-red utility class .card .title से हार जाती है क्योंकि बाद वाली की specificity ज़्यादा है। Fix हमेशा एक ही था: selectors और specific बनाओ, !important जोड़ो, या सब restructure करो।

हमने पूरी methodologies (BEM, SMACSS, ITCSS) और toolchains बनाईं सिर्फ़ specificity conflicts से बचने के लिए। यह सब एक missing language feature का workaround था।

Layer Ordering#

@layer आपको declare करने देता है कि styles के groups किस क्रम में consider हों, उन groups के अंदर specificity से बेपरवाह:

css
/* Layer order declare करें — यह एक line सब control करती है */
@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 की specificity .text-red से ज़्यादा है, utility जीतती है क्योंकि utilities layer components के बाद declare है। कोई !important नहीं। कोई specificity hacks नहीं। Layer order ही अंतिम फैसला है।

Tailwind CSS v4 Layers कैसे इस्तेमाल करता है#

Tailwind v4 @layer पर बहुत निर्भर है। जब @import "tailwindcss" लिखते हैं, तो मिलता है:

css
@layer theme, base, components, utilities;

हर Tailwind utility utilities layer में रहती है। Custom component styles components में जाती हैं। यही कारण है कि text-red-500 class !important के बिना component का color override कर सकती है — यह बाद वाली layer में है।

अगर Tailwind के बिना अपना design system बना रहे हैं, यह architecture चुरा लीजिए। यही सही है:

css
@layer reset, tokens, base, layouts, components, utilities, overrides;

सात layers काफ़ी हैं। मुझे कभी ज़्यादा नहीं चाहिए।

Unlayered Styles सबसे ऊपर जीतती हैं#

एक चेतावनी: जो styles किसी भी layer में नहीं हैं उनकी priority सबसे ज़्यादा होती है। यह वास्तव में उपयोगी है — इसका मतलब आपकी one-off page-specific overrides automatically जीतती हैं:

css
@layer components {
  .modal {
    background: white;
  }
}
 
/* किसी layer में नहीं — layers में सब कुछ पर जीतता है */
.special-page .modal {
  background: oklch(95% 0.02 260);
}

लेकिन इसका मतलब third-party CSS जो layer-aware नहीं है वो आपका पूरा system override कर सकती है। Third-party styles को layer में wrap करें control करने के लिए:

css
@layer third-party {
  @import url("some-library.css");
}

:has() Selector: वो Parent Selector जो हम हमेशा चाहते थे#

सचमुच दशकों से, developers parent selector मांग रहे थे। "मैं parent को उसके children के आधार पर style करना चाहता हूं।" जवाब हमेशा था "CSS ऐसा नहीं कर सकता" इसके बाद JavaScript workaround। :has() यह पूरी तरह बदलता है, और पता चला कि यह उससे भी ज़्यादा शक्तिशाली है जो हमने मांगा था।

बुनियादी Parent Selection#

css
/* Form group को style करें जब उसके input पर 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);
}
 
/* Card को अलग style करें जब उसमें image हो */
.card:has(img) {
  padding-top: 0;
}
 
.card:has(img) .card-content {
  padding: 1.5rem;
}
 
/* बिना image वाले card को अलग treatment */
.card:not(:has(img)) {
  border-left: 4px solid oklch(55% 0.2 260);
}

JavaScript के बिना Form Validation States#

यहां :has() वास्तव में रोमांचक हो जाता है। HTML validation pseudo-classes के साथ मिलकर, शून्य JavaScript से form UIs बना सकते हैं जो validity state पर respond करें:

css
/* Field wrapper अपने input की validity पर react करता है */
.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) हिस्सा ज़रूरी है — यह खाली fields पर validation styles दिखने से रोकता है जिन्हें अभी छुआ ही नहीं गया।

Children Count के आधार पर Layout#

यह pattern बेहद उपयोगी है और :has() से पहले वास्तव में असंभव था:

css
/* कितने items हैं उसके आधार पर grid columns adjust करें */
.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 अपने children की संख्या के आधार पर column count बदलता है। कोई JavaScript नहीं। कोई ResizeObserver नहीं। कोई class toggling नहीं।

मेरे पसंदीदा patterns में से एक — sidebar मौजूद है या नहीं इसके आधार पर layout बदलना:

css
.page-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
}
 
/* अगर layout में sidebar है, दो columns पर switch करें */
.page-layout:has(.sidebar) {
  grid-template-columns: 1fr 300px;
}
 
/* Sidebar होने पर main content width adjust करें */
.page-layout:has(.sidebar) .main-content {
  max-width: 65ch;
}

DOM में sidebar component जोड़ो, layout adjust हो जाता है। हटाओ, फिर adjust हो जाता है। CSS truth का source है, कोई state variable नहीं।

:has() को अन्य Selectors के साथ Combine करना#

:has() बाकी सब के साथ खूबसूरती से compose होता है:

css
/* Article को तभी style करें जब उसमें specific class का figure हो */
article:has(figure.full-bleed) {
  overflow: visible;
}
 
/* Navigation जो search input focus होने पर बदले */
nav:has(input[type="search"]:focus) {
  background: oklch(98% 0 0);
  box-shadow: 0 2px 8px oklch(0% 0 0 / 0.08);
}
 
/* Component level पर dark-mode enable करें */
.theme-switch:has(input:checked) ~ main {
  color-scheme: dark;
  background: oklch(15% 0 0);
  color: oklch(90% 0 0);
}

Browser support अब उत्कृष्ट है। हर modern browser ने 2024 की शुरुआत से :has() support किया है। इसे इस्तेमाल न करने का कोई कारण नहीं।

CSS Nesting: Native, आखिरकार Stable#

मैं इसे ज़्यादा नहीं बेचूंगा। CSS nesting अच्छा है। यह container queries या :has() की तरह क्रांतिकारी नहीं है। लेकिन यह Sass इस्तेमाल करने की आखिरी वजहों में से एक हटा देता है, और वो मायने रखता है।

Syntax#

css
.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);
  }
}

यह valid CSS है। कोई build step नहीं। कोई preprocessor नहीं। कोई PostCSS plugin नहीं। Browser natively handle करता है।

Sass से अंतर#

कुछ syntax अंतर जानने लायक हैं:

css
/* CSS Nesting — अब सभी browsers में काम करता है */
.parent {
  /* Direct class/element nesting & के बिना काम करता है */
  .child {
    color: red;
  }
 
  /* Pseudo-classes और compound selectors के लिए & ज़रूरी */
  &:hover {
    background: blue;
  }
 
  &.active {
    font-weight: bold;
  }
 
  /* Nested media queries — यह शानदार है */
  @media (width >= 768px) {
    flex-direction: row;
  }
}

शुरुआती implementations में, element selectors से पहले & चाहिए था (जैसे p की बजाय & p)। यह प्रतिबंध हट गया। 2025 तक, सभी प्रमुख browsers bare element nesting support करते हैं: .parent { p { ... } } ठीक काम करता है।

Nested Media Queries#

यह CSS nesting की killer feature है, मेरी राय में। Selector nesting नहीं — media queries को rule block के अंदर रखने की क्षमता:

css
.hero {
  padding: 2rem;
  font-size: 1rem;
 
  @media (width >= 768px) {
    padding: 4rem;
    font-size: 1.25rem;
  }
 
  @media (width >= 1200px) {
    padding: 6rem;
    font-size: 1.5rem;
  }
}

पुराने तरीके से तुलना करें जहां .hero styles तीन अलग @media blocks में बिखरी होतीं, शायद सैकड़ों lines दूर। Nesting responsive behavior को component के साथ co-located रखता है। Readability नाटकीय रूप से बेहतर होती है।

ज़्यादा Nest मत करो#

एक चेतावनी: सिर्फ़ इसलिए कि छह levels गहरे nest कर सकते हैं इसका मतलब नहीं कि करना चाहिए। Sass की वही सलाह यहां भी लागू है। अगर nesting .page .section .card .content .text .highlight जैसे selectors बनाता है, आपने specificity monster और maintenance nightmare बना दिया। दो-तीन levels sweet spot है।

css
/* अच्छा — दो levels */
.nav {
  .link {
    color: inherit;
 
    &:hover {
      color: oklch(55% 0.2 260);
    }
  }
}
 
/* बुरा — specificity nightmare */
.page {
  .layout {
    .sidebar {
      .nav {
        .list {
          .item {
            .link {
              color: red; /* इसे override करने में शुभकामनाएं */
            }
          }
        }
      }
    }
  }
}

Color Functions: oklch() और color-mix() सब बदल देते हैं#

hsl() ने अच्छा काम किया। rgb() से ज़्यादा intuitive था। लेकिन इसमें एक मूलभूत खामी है: यह perceptually uniform नहीं है। hsl(60, 100%, 50%) (पीला) इंसानी आंख को hsl(240, 100%, 50%) (नीला) से नाटकीय रूप से हल्का दिखता है, भले ही दोनों में same lightness value है।

oklch() क्यों जीतता है#

oklch() perceptually uniform है। बराबर lightness values दिखती बराबर हल्की। Color palettes generate करने, themes बनाने, और accessible contrast ensure करने में यह बहुत मायने रखता है:

css
:root {
  /* oklch(lightness chroma hue) */
  --color-primary: oklch(55% 0.2 260);     /* नीला */
  --color-secondary: oklch(55% 0.2 330);   /* बैंगनी */
  --color-success: oklch(55% 0.2 145);     /* हरा */
  --color-danger: oklch(55% 0.25 25);      /* लाल */
  --color-warning: oklch(75% 0.18 85);     /* पीला — बराबर perception के लिए ज़्यादा L */
 
  /* ये सब इंसानी आंख को बराबर "medium" दिखते हैं */
  /* HSL के साथ, हर hue के लिए अलग lightness values चाहिए */
}

तीन values समझने पर intuitive हैं:

  • Lightness (0% से 100%): कितना हल्का या गहरा। 0% काला, 100% सफ़ेद।
  • Chroma (0 से ~0.37): कितना जीवंत। 0 grey है, ज़्यादा ज़्यादा saturated।
  • Hue (0 से 360): रंग कोण। 0/360 गुलाबी लाल, 145 हरा, 260 नीला।

Derived Colors के लिए color-mix()#

color-mix() runtime पर दूसरे रंगों से रंग बनाने देता है। कोई Sass darken() function नहीं। कोई JavaScript नहीं। बस CSS:

css
:root {
  --brand: oklch(55% 0.2 260);
 
  /* White में mix करके हल्का करें */
  --brand-light: color-mix(in oklch, var(--brand) 30%, white);
 
  /* Black में mix करके गहरा करें */
  --brand-dark: color-mix(in oklch, var(--brand) 70%, black);
 
  /* 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 हिस्सा मायने रखता है। srgb में mix करने पर गंदले intermediate colors आते हैं। oklch में mix करने पर perceptually even results। हमेशा oklch में mix करें।

oklch() से पूरी Palette बनाना#

देखिए मैं एक hue से पूरी shade palette कैसे generate करता हूं:

css
: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 को 145 बदलो और हरी palette मिलती है। 25 बदलो और लाल। Lightness steps perceptually even हैं। Chroma extremes पर taper करता है ताकि सबसे हल्के और गहरे shades oversaturated न हों। पहले इसके लिए design tool या Sass function चाहिए होता था। अब आठ lines CSS।

Scroll-Driven Animations: कोई JavaScript ज़रूरत नहीं#

यह वो feature है जिसने मुझसे सबसे ज़्यादा JavaScript delete करवाया। Scroll-linked animations — progress bars, parallax effects, reveal animations, sticky headers with transitions — पहले IntersectionObserver, scroll event listeners, या GSAP जैसी library चाहिए थी। अब CSS है।

Scroll Progress Indicator#

Classic "reading progress bar" article page के ऊपर:

css
.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);
  }
}

बस। पूरा reading progress indicator। कोई JavaScript नहीं। कोई scroll event listener नहीं। कोई requestAnimationFrame नहीं। कोई "percentage scrolled" calculation नहीं। animation-timeline: scroll() binding सब कुछ करती है।

Scroll पर Reveal#

Elements जो viewport में आने पर fade in हों:

css
.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() animation को viewport में element की visibility से जोड़ता है। animation-range: entry 0% entry 100% मतलब animation तब चलता है जब element viewport में enter करना शुरू करता है तब तक जब पूरी तरह visible हो।

Library के बिना Parallax#

css
.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;
}

Background image scroll से अलग rate पर move करती है, parallax effect बनाती है। Smooth, performant (browser GPU पर composite कर सकता है), और शून्य JavaScript।

Named Scroll Timelines#

ज़्यादा control के लिए, scroll timelines को नाम दे सकते हैं और दूसरे elements से reference कर सकते हैं:

css
.scroller {
  overflow-y: auto;
  scroll-timeline-name: --my-scroller;
  scroll-timeline-axis: block;
}
 
/* DOM में कहीं भी element इस timeline को reference कर सकता है */
.indicator {
  animation: progress linear both;
  animation-timeline: --my-scroller;
}
 
@keyframes progress {
  from { width: 0%; }
  to { width: 100%; }
}

यह तब भी काम करता है जब indicator और scroller parent/child नहीं। कोई भी element किसी भी named scroll timeline से link हो सकता है। Dashboard UIs के लिए शक्तिशाली जहां scrollable panel fixed header में indicator drive करे।

Anchor Positioning: Tooltips और Popovers सही तरीके से#

Anchor positioning से पहले, tooltip को उसके trigger element से जोड़ने के लिए JavaScript चाहिए था। getBoundingClientRect() से positions calculate करनी होतीं, scroll offsets handle करने होते, viewport collisions manage करने होते, और resize पर फिर से calculate करना होता। Popper.js (अब Floating UI) जैसी libraries इसलिए exist करती थीं क्योंकि यह सही करना इतना कठिन था।

CSS anchor positioning इसे declarative बनाता है:

css
.trigger {
  anchor-name: --my-trigger;
}
 
.tooltip {
  position: fixed;
  position-anchor: --my-trigger;
 
  /* Tooltip का top-center trigger के bottom-center पर position करें */
  top: anchor(bottom);
  left: anchor(center);
  translate: -50% 0;
 
  /* Viewport overflow होने पर fallback positioning */
  position-try-fallbacks: flip-block, flip-inline;
}

Automatic Viewport Collision Handling#

position-try-fallbacks property वो हिस्सा है जिसमें JavaScript की 200 lines लगतीं। यह browser को बताता है: "अगर tooltip viewport के नीचे overflow हो, ऊपर flip करो। अगर दाएं overflow हो, बाएं flip करो।"

css
.dropdown-menu {
  position: fixed;
  position-anchor: --menu-button;
 
  /* Default: button के नीचे, left edge से aligned */
  top: anchor(bottom);
  left: anchor(left);
 
  /* नीचे fit न हो तो ऊपर try करो। Left-aligned fit न हो तो right-aligned */
  position-try-fallbacks: flip-block, flip-inline;
 
  /* Anchor और dropdown के बीच gap */
  margin-top: 4px;
}

Named Position Fallbacks#

Fallback positions पर ज़्यादा control के लिए, custom try options define कर सकते हैं:

css
@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 हर fallback क्रम में try करता है जब तक ऐसा न मिले जो element को viewport में रखे। इस तरह की spatial reasoning JavaScript में वास्तव में दर्दनाक थी।

Popover API के साथ Anchoring#

Anchor positioning नए Popover API के साथ एकदम सही जोड़ी बनाता है:

html
<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 यहां
</div>

Show/hide के लिए कोई JavaScript नहीं (Popover API handle करता है)। Positioning के लिए कोई JavaScript नहीं (anchor positioning handle करता है)। Light-dismiss behavior के लिए कोई JavaScript नहीं (Popover API वो भी handle करता है)। पूरा tooltip/popover/dropdown pattern — जिसने पूरे npm packages power किए — अब HTML और CSS है।

CSS Grid Subgrid: Nested Alignment जो वाकई काम करता है#

Grid शक्तिशाली है, लेकिन इसमें एक frustrating सीमा थी: child grid अपने items को parent grid से align नहीं कर सकता था। अगर cards की row हो और हर card के title, content, और footer cards के across align चाहिए, कोई रास्ता नहीं था। हर card का internal grid independent था।

Subgrid इसे ठीक करता है।

Card Alignment समस्या#

css
/* Parent grid */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}
 
/* हर card subgrid बनता है, rows को parent से align करता है */
.card {
  display: grid;
  grid-row: span 3; /* Card parent में 3 rows span करता है */
  grid-template-rows: subgrid;
  gap: 0; /* Card अपना internal gap control करता है */
}
 
.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);
}

अब तीनों cards के titles एक ही row पर align होते हैं। Bodies align होती हैं। Footers align होते हैं। भले ही एक card में दो-line title हो और दूसरे में एक-line, alignment parent grid maintain करता है।

Subgrid से पहले: Hacks#

Subgrid के बिना, यह alignment हासिल करने के लिए या तो:

  1. Fixed heights (fragile, dynamic content से टूट जाता है)
  2. JavaScript measuring (धीमा, misalignment का flash)
  3. हार मान लो और misalignment accept करो (आम, बदसूरत)
css
/* पुराना hack — fragile और dynamic content से टूटता है */
.card-title {
  min-height: 3rem; /* प्रार्थना करो कोई title इससे बड़ा न हो */
}
 
.card-body {
  min-height: 8rem; /* और प्रार्थना */
}

Subgrid hack को अनावश्यक बनाता है। Parent grid सभी cards में हर row की सबसे लंबी content के आधार पर row heights distribute करता है।

Form Layouts के लिए Column Subgrid#

Subgrid columns पर भी काम करता है, जो form layouts के लिए एकदम सही है:

css
.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;
}

सभी labels align। सभी inputs align। Label column सबसे चौड़े label के अनुसार auto-size होता है। कोई hardcoded widths नहीं।

View Transitions API: Framework के बिना SPA-जैसा Navigation#

यह इस list का सबसे महत्वाकांक्षी feature है। View Transitions API page navigations के बीच animate करने देता है — cross-document navigations (multi-page app में सामान्य link clicks) सहित। आपकी static HTML site में अब smooth, animated page transitions हो सकते हैं।

Cross-Document Transitions#

Cross-document view transitions enable करने के लिए, पुराने और नए दोनों page पर एक CSS rule जोड़ें:

css
@view-transition {
  navigation: auto;
}

बस। Browser अब pages navigate करते समय cross-fade करेगा। Default transition smooth opacity fade है। कोई JavaScript नहीं। कोई framework नहीं। दोनों pages पर बस दो lines CSS।

Transition Customize करना#

Specific elements नाम देकर transitions customize कर सकते हैं:

css
/* दोनों 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; /* नया hero बस दिख जाता है */
}
 
@keyframes slide-out {
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}
 
@keyframes slide-in {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
}
 
@keyframes fade-out {
  to {
    opacity: 0;
  }
}

जब दो pages navigate करें जिन दोनों में .hero-image है view-transition-name: hero के साथ, browser automatically image को पुराने page की position से नए page की position तक animate करता है। Mobile development का "shared element transition" pattern, अब browser में।

SPA View Transitions#

Single-page apps (React, Vue, Svelte, आदि) के लिए, JavaScript API सीधा है:

css
/* CSS side — transition animations define करें */
::view-transition-old(root) {
  animation: fade-out 0.2s ease-in;
}
 
::view-transition-new(root) {
  animation: fade-in 0.2s ease-out;
}
javascript
// JS side — DOM update को startViewTransition में wrap करें
document.startViewTransition(() => {
  // DOM यहां update करें — React render, innerHTML swap, आदि
  updateContent(newPageData);
});

Browser पुरानी state snapshot लेता है, update चलाता है, नई state snapshot लेता है, और बीच में animate करता है। Named elements को individual transitions मिलती हैं; बाकी सबको default crossfade।

User Preferences का सम्मान#

हमेशा prefers-reduced-motion का सम्मान करें:

css
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

क्या इस्तेमाल करना बंद कर सकते हैं#

ये features सामूहिक रूप से बहुत सारी tooling, libraries, और patterns eliminate कर देते हैं जिन पर हम सालों से निर्भर थे। यहां बताता हूं मैंने क्या हटाया या किसके लिए हाथ बढ़ाना बंद किया:

Nesting और Variables के लिए Sass/SCSS#

CSS में native nesting है। CSS में custom properties हैं (और सालों से हैं)। Sass के लिए लोग जो दो मुख्य कारण ढूंढते थे अब भाषा में हैं। अगर अभी भी Sass सिर्फ़ $variables और nesting के लिए इस्तेमाल कर रहे हैं, बंद कर सकते हैं।

css
/* पहले: Sass */
$primary: #3b82f6;
 
.card {
  background: white;
  border: 1px solid lighten($primary, 40%);
 
  &:hover {
    border-color: $primary;
  }
 
  .title {
    color: darken($primary, 15%);
  }
}
 
/* अब: 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 version ज़्यादा शक्तिशाली है — oklch perceptually uniform color manipulation देता है, color-mix runtime पर काम करता है (Sass lighten सिर्फ़ compile-time), और custom properties JavaScript या media queries से dynamically बदल सकती हैं।

Scroll Animations के लिए JavaScript#

अपना IntersectionObserver reveal-on-scroll code delete करें। Scroll progress bar JavaScript delete करें। Parallax scroll handler delete करें। animation-timeline: scroll() और animation-timeline: view() ये सब बेहतर performance (compositor thread, main thread नहीं) के साथ handle करते हैं।

javascript
// पहले: JavaScript जो 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));
css
/* अब: CSS जो ऊपर सब replace करता है */
.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); }
}

Positioning के लिए Popper.js / Floating UI#

अगर Floating UI (पहले Popper.js) सिर्फ़ basic tooltip/popover positioning के लिए इस्तेमाल कर रहे हैं, CSS anchor positioning replace करता है। कुछ advanced features (virtual elements, custom middleware) खो देंगे, लेकिन 90% use case — "यह popover उस button के पास रखो और viewport में रखो" — CSS अब natively करता है।

Component Responsiveness के लिए Media Queries#

यह बड़ी बात है। अगर component responsive बनाने के लिए @media queries लिख रहे हैं, अब शायद गलत कर रहे हैं। Component-level responsiveness के लिए container queries default होनी चाहिए। @media queries page-level decisions के लिए रखें: layout changes, navigation patterns, print styles।

css
/* पहले: Component styling के लिए media query (गलत scope) */
@media (min-width: 768px) {
  .product-card {
    flex-direction: row;
  }
}
 
/* अब: Container query (सही scope) */
.product-card-wrapper {
  container-type: inline-size;
}
 
@container (min-width: 400px) {
  .product-card {
    flex-direction: row;
  }
}

बड़ी तस्वीर#

ये features अलगाव में exist नहीं करते। ये compose होते हैं। Container queries + :has() + nesting + oklch() + layers — साथ इस्तेमाल करें — ऐसा CSS authoring experience देते हैं जो पांच साल पहले पहचाना नहीं जा सकता था:

css
@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;
      }
    }
  }
}

वो एक block handle करता है:

  • Component-level responsive layout (container queries)
  • Content के आधार पर conditional styling (:has())
  • साफ़ selector organization (nesting)
  • Perceptually uniform colors (oklch)
  • Predictable specificity management (@layer)

कोई preprocessor नहीं। कोई CSS-in-JS runtime नहीं। इसके लिए कोई utility classes नहीं। बस CSS, वो करते हुए जो CSS को हमेशा करना चाहिए था।

Platform catch up कर गया। आखिरकार। और इंतज़ार worth था।

संबंधित पोस्ट