CSS: caratteristiche moderne

Il CSS ha subito una trasformazione profonda negli ultimi anni. Quelle che un tempo erano soluzioni affidate a preprocessori come Sass e Less, oppure a librerie JavaScript, sono oggi funzionalità native del linguaggio. Le specifiche CSS più recenti hanno introdotto strumenti che ridefiniscono il modo in cui gli sviluppatori costruiscono layout, gestiscono la responsività, organizzano gli stili e creano animazioni. Questo articolo esplora in dettaglio le caratteristiche moderne che ogni sviluppatore front-end dovrebbe conoscere e padroneggiare.

Custom Properties (variabili CSS)

Le Custom Properties, comunemente note come variabili CSS, rappresentano una delle aggiunte più significative al linguaggio. A differenza delle variabili dei preprocessori, le Custom Properties sono entità vive nel DOM: possono essere ereditate, sovrascritte in contesti specifici e manipolate tramite JavaScript a runtime.

Una Custom Property viene dichiarata con il prefisso -- e richiamata tramite la funzione var(). La dichiarazione avviene tipicamente nel selettore :root per garantire una disponibilità globale, ma può essere effettuata in qualsiasi selettore per creare scope locali.

/* Definizione globale delle variabili */
:root {
  --color-primary: #1a73e8;
  --color-secondary: #fbbc04;
  --spacing-base: 1rem;
  --font-family-body: 'Segoe UI', system-ui, sans-serif;
  --border-radius-default: 0.5rem;
}

/* Sovrascrittura locale delle variabili */
.card {
  --color-primary: #e91e63;
  background-color: var(--color-primary);
  padding: var(--spacing-base);
  border-radius: var(--border-radius-default);
}

/* Valore di fallback nel caso la variabile non sia definita */
.banner {
  color: var(--color-accent, #333);
}

La potenza delle Custom Properties emerge in modo evidente quando vengono combinate con i media query o con la manipolazione via JavaScript, permettendo di creare sistemi di theming dinamici senza ricorrere a classi aggiuntive.

/* Tema scuro tramite media query */
@media (prefers-color-scheme: dark) {
  :root {
    --color-primary: #8ab4f8;
    --color-background: #1e1e1e;
    --color-text: #e0e0e0;
  }
}

// Modifica della variabile tramite JavaScript
const root = document.documentElement;
root.style.setProperty('--color-primary', '#ff5722');

Container Queries

Per anni gli sviluppatori hanno desiderato poter applicare stili in base alle dimensioni del contenitore anziché a quelle del viewport. Le Container Queries rispondono esattamente a questa esigenza, rendendo i componenti realmente autonomi e riutilizzabili in contesti diversi.

Per utilizzare le Container Queries è necessario dichiarare un elemento come contesto di contenimento tramite la proprietà container-type. Successivamente, si utilizza la regola @container per applicare stili condizionali basati sulle dimensioni di quel contenitore.

/* Dichiarazione del contenitore */
.widget-wrapper {
  container-type: inline-size;
  container-name: widget;
}

/* Stili condizionali basati sulla larghezza del contenitore */
@container widget (min-width: 400px) {
  .widget-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }
}

@container widget (max-width: 399px) {
  .widget-content {
    display: flex;
    flex-direction: column;
  }
}

La proprietà abbreviata container consente di specificare sia il nome che il tipo in un'unica dichiarazione.

/* Forma abbreviata */
.sidebar {
  container: sidebar / inline-size;
}

Le Container Queries rappresentano un cambio di paradigma nello sviluppo di componenti, poiché spostano la responsabilità della responsività dal livello globale della pagina al livello locale del singolo componente.

Nesting nativo

Il nesting, ossia la possibilità di annidare regole CSS all'interno di altre regole, è stata una delle ragioni principali per adottare preprocessori come Sass. Oggi il CSS supporta il nesting in modo nativo, riducendo la necessità di strumenti esterni e migliorando la leggibilità del codice.

/* Nesting nativo senza preprocessori */
.navigation {
  background-color: #fff;
  padding: 1rem 2rem;

  /* Selettore annidato per gli elementi della lista */
  ul {
    list-style: none;
    display: flex;
    gap: 1.5rem;
    margin: 0;
    padding: 0;
  }

  /* Selettore annidato per i link */
  a {
    text-decoration: none;
    color: #333;
    font-weight: 500;

    /* Pseudo-classe annidata */
    &:hover {
      color: #1a73e8;
    }

    /* Pseudo-classe per il link attivo */
    &.active {
      border-bottom: 2px solid currentColor;
    }
  }

  /* Media query annidato */
  @media (max-width: 768px) {
    padding: 0.5rem 1rem;

    ul {
      flex-direction: column;
      gap: 0.5rem;
    }
  }
}

Il simbolo & fa riferimento al selettore genitore e diventa indispensabile quando si lavora con pseudo-classi, pseudo-elementi o selettori composti. Il nesting nativo supporta anche l'annidamento di media query e altre at-rules direttamente all'interno dei blocchi di regole.

La funzione :has()

La pseudo-classe :has() è stata definita da molti come il "selettore genitore" del CSS, una funzionalità richiesta dalla comunità per oltre un decennio. In realtà, :has() è molto più versatile: è un selettore relazionale che permette di selezionare un elemento in base alla presenza di specifici discendenti, fratelli o stati all'interno del suo sottoalbero.

/* Seleziona un elemento che contiene un'immagine */
.card:has(img) {
  padding: 0;
  overflow: hidden;
}

/* Seleziona un elemento che contiene un input in stato di focus */
.form-group:has(input:focus) {
  border-color: #1a73e8;
  box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);
}

/* Seleziona un elemento che NON contiene paragrafi */
.container:has(:not(p)) {
  min-height: 200px;
}

/* Seleziona il label che precede un input richiesto */
label:has(+ input:required)::after {
  content: ' *';
  color: #d32f2f;
}

/* Stili globali basati sullo stato di un elemento specifico */
body:has(.modal.open) {
  overflow: hidden;
}

La potenza di :has() risiede nel fatto che consente di esprimere relazioni tra elementi che prima richiedevano JavaScript. Validazione visiva dei form, layout condizionali e stati globali della pagina possono ora essere gestiti interamente con CSS.

Subgrid

CSS Grid ha rivoluzionato il modo in cui si costruiscono layout complessi, ma presentava un limite importante: i figli diretti di un grid container potevano partecipare alla griglia, ma i loro discendenti no. Il valore subgrid risolve questo problema, consentendo agli elementi annidati di allinearsi alla griglia del loro antenato.

/* Griglia principale */
.product-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
}

/* Ogni carta eredita le tracce della riga dalla griglia principale */
.product-card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3; /* Occupa tre righe della griglia genitore */
}

/* Gli elementi interni si allineano alla griglia genitore */
.product-card .product-image {
  align-self: stretch;
}

.product-card .product-title {
  align-self: start;
}

.product-card .product-price {
  align-self: end;
}

Il valore subgrid è particolarmente utile nelle griglie di schede (card grids), dove è necessario che elementi come titoli, descrizioni e pulsanti si allineino orizzontalmente tra schede diverse, indipendentemente dalla lunghezza del contenuto.

Funzioni di colore moderne

Il CSS moderno ha introdotto nuovi spazi colore e funzioni che superano i limiti storici di rgb() e hsl(). Le funzioni oklch() e oklab() offrono una rappresentazione dei colori percettivamente uniforme, mentre color-mix() permette di miscelare colori direttamente nel foglio di stile.

/* Colori in OKLCH: luminosità, croma, tonalità */
:root {
  --color-brand: oklch(0.65 0.25 260);
  --color-brand-light: oklch(0.85 0.15 260);
  --color-brand-dark: oklch(0.40 0.25 260);
}

/* Miscelazione di colori con color-mix() */
.overlay {
  /* Miscela al 30% il colore primario con il nero */
  background-color: color-mix(in oklch, var(--color-brand) 30%, black);
}

/* Creazione di varianti tonali */
.button-hover {
  background-color: color-mix(in oklch, var(--color-brand), white 20%);
}

/* Utilizzo di colori con trasparenza relativa */
.backdrop {
  background-color: oklch(0.3 0.05 260 / 0.8);
}

Lo spazio colore OKLCH è superiore ad HSL per la creazione di palette coerenti, poiché garantisce che colori con la stessa luminosità dichiarata appaiano effettivamente della stessa luminosità percepita all'occhio umano.

Scroll-driven Animations

Le animazioni guidate dallo scroll rappresentano una delle novità più entusiasmanti del CSS moderno. Grazie alle proprietà animation-timeline, scroll-timeline e view-timeline, è possibile collegare le animazioni CSS al progresso dello scroll senza una sola riga di JavaScript.

/* Barra di progresso collegata allo scroll della pagina */
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background-color: #1a73e8;
  transform-origin: left;
  /* Collegamento dell'animazione allo scroll */
  animation: grow-progress linear;
  animation-timeline: scroll();
}

@keyframes grow-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

/* Animazione di entrata collegata alla visibilità dell'elemento */
.fade-in-element {
  animation: fade-in linear both;
  animation-timeline: view();
  /* Intervallo di attivazione dell'animazione */
  animation-range: entry 0% entry 100%;
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(50px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Le scroll-driven animations eliminano la necessità di librerie come Intersection Observer per le animazioni di entrata e di librerie dedicate per gli effetti di parallasse, con un impatto positivo sulle prestazioni poiché le animazioni CSS vengono eseguite sul thread del compositor.

Anchor Positioning

L'Anchor Positioning è una specifica CSS che consente di posizionare un elemento relativamente a un altro elemento specifico (l'ancora), indipendentemente dalla loro relazione nel DOM. Questa funzionalità è fondamentale per tooltip, popover, menu a tendina e qualsiasi elemento che debba "agganciarsi" a un punto di riferimento.

/* Definizione dell'ancora */
.trigger-button {
  anchor-name: --tooltip-anchor;
}

/* Posizionamento dell'elemento ancorato */
.tooltip {
  position: fixed;
  position-anchor: --tooltip-anchor;
  /* Posizionamento sopra l'ancora, centrato orizzontalmente */
  top: anchor(top);
  left: anchor(center);
  translate: -50% calc(-100% - 8px);
  /* Fallback automatico se non c'è spazio */
  position-try-fallbacks: flip-block, flip-inline;
}

La dichiarazione position-try-fallbacks permette di specificare posizioni alternative che il browser può utilizzare automaticamente quando la posizione preferita causerebbe un overflow del viewport, eliminando la necessità di calcoli JavaScript per il riposizionamento dinamico.

La regola @layer

La gestione della specificità è sempre stata una sfida nello sviluppo CSS su larga scala. La regola @layer introduce i Cascade Layers, un meccanismo che permette di organizzare gli stili in livelli con una gerarchia di priorità esplicita, indipendente dalla specificità dei selettori o dall'ordine di apparizione nel codice sorgente.

/* Dichiarazione dell'ordine dei livelli */
@layer reset, base, components, utilities;

/* Stili di reset nel livello con priorità più bassa */
@layer reset {
  *,
  *::before,
  *::after {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
}

/* Stili di base */
@layer base {
  body {
    font-family: var(--font-family-body);
    line-height: 1.6;
    color: #333;
  }

  a {
    color: var(--color-primary);
  }
}

/* Componenti con specificità alta che non sovrascrivono le utility */
@layer components {
  .card {
    padding: 2rem;
    border: 1px solid #e0e0e0;
    border-radius: var(--border-radius-default);
  }

  .card a {
    color: inherit;
    text-decoration: underline;
  }
}

/* Le utility hanno sempre la priorità, anche con bassa specificità */
@layer utilities {
  .text-center {
    text-align: center;
  }

  .hidden {
    display: none;
  }
}

Con @layer, anche un selettore con specificità bassa nel livello utilities avrà la precedenza su un selettore con specificità alta nel livello components, poiché la priorità dei livelli è determinata dall'ordine di dichiarazione. Questo risolve elegantemente il problema delle "guerre di specificità" nei progetti complessi.

La regola @scope

La regola @scope permette di definire un ambito di applicazione per gli stili CSS, limitando la loro portata a un sottoalbero specifico del DOM. Questo è particolarmente utile per i componenti che devono avere stili isolati senza ricorrere a metodologie come BEM o a soluzioni CSS-in-JS.

/* Stili limitati all'ambito del componente */
@scope (.media-card) {
  /* Seleziona solo gli elementi h2 dentro .media-card */
  h2 {
    font-size: 1.25rem;
    margin-bottom: 0.5rem;
  }

  p {
    color: #666;
    line-height: 1.5;
  }

  img {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
  }
}

/* Ambito con limite inferiore per escludere sotto-componenti */
@scope (.dashboard) to (.widget) {
  /* Si applica dentro .dashboard ma NON dentro .widget */
  h2 {
    font-size: 2rem;
    color: var(--color-primary);
  }
}

La sintassi @scope (root) to (limit) definisce un ambito con un limite inferiore: gli stili si applicano ai discendenti di root ma si fermano prima di raggiungere gli elementi che corrispondono a limit, creando una sorta di "barriera" stilistica.

Funzioni matematiche avanzate

Oltre alla nota funzione calc(), il CSS moderno offre un insieme completo di funzioni matematiche che consentono di creare layout e tipografia fluidi con un controllo molto fine.

/* clamp() per una tipografia fluida */
h1 {
  /* Minimo 1.5rem, preferito 4vw, massimo 3rem */
  font-size: clamp(1.5rem, 4vw, 3rem);
}

/* min() e max() per layout adattivi */
.content-wrapper {
  /* Non supera mai 1200px e ha sempre un margine laterale */
  width: min(90%, 1200px);
  margin-inline: auto;
}

.sidebar {
  /* Almeno 250px, indipendentemente dallo spazio disponibile */
  width: max(250px, 25%);
}

/* round() per allineare i valori alla griglia */
.grid-element {
  width: round(nearest, 100%, 120px);
}

/* Funzioni trigonometriche per disposizioni circolari */
.circular-item {
  --angle: calc(var(--index) * 60deg);
  /* Calcolo delle coordinate con seno e coseno */
  translate: calc(cos(var(--angle)) * 150px)
             calc(sin(var(--angle)) * 150px);
}

La funzione clamp() è diventata uno strumento fondamentale per la tipografia fluida, poiché consente di definire un intervallo di valori che si adatta automaticamente al viewport senza la necessità di breakpoint multipli.

Nuovi selettori e pseudo-classi

Il CSS moderno ha arricchito significativamente il repertorio di selettori disponibili, aggiungendo pseudo-classi che esprimono stati e relazioni precedentemente inesprimibili.

/* :is() per raggruppare selettori con specificità del selettore più specifico */
:is(h1, h2, h3, h4) {
  font-family: 'Georgia', serif;
  line-height: 1.2;
}

/* :where() identico a :is() ma con specificità zero */
:where(h1, h2, h3, h4) {
  margin-bottom: 1rem;
}

/* :not() con selettori multipli */
input:not([type="submit"], [type="reset"]) {
  border: 1px solid #ccc;
  padding: 0.5rem;
}

/* :focus-visible per lo stile del focus solo da tastiera */
button:focus-visible {
  outline: 3px solid #1a73e8;
  outline-offset: 2px;
}

/* :focus-within per stilare il genitore quando un figlio ha il focus */
.search-box:focus-within {
  border-color: #1a73e8;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Selezione dell'ultimo elemento figlio di un tipo specifico */
.list-item:last-of-type {
  border-bottom: none;
}

/* :user-valid e :user-invalid per la validazione dopo l'interazione */
input:user-valid {
  border-color: #2e7d32;
}

input:user-invalid {
  border-color: #d32f2f;
}

La distinzione tra :is() e :where() è cruciale: mentre :is() assume la specificità del selettore più specifico nella lista, :where() ha sempre specificità zero, rendendolo ideale per gli stili di base che devono essere facilmente sovrascrivibili.

Proprietà logiche

Le proprietà logiche sostituiscono le tradizionali proprietà fisiche (top, right, bottom, left) con equivalenti che si adattano automaticamente alla direzione di scrittura del documento. Questo è essenziale per l'internazionalizzazione, in particolare per le lingue con scrittura da destra a sinistra (RTL).

/* Proprietà logiche al posto di quelle fisiche */
.text-block {
  /* Equivalente di margin-top e margin-bottom */
  margin-block: 1rem 2rem;
  /* Equivalente di margin-left e margin-right */
  margin-inline: auto;
  /* Equivalente di padding-left e padding-right */
  padding-inline: 1.5rem;
  /* Equivalente di padding-top e padding-bottom */
  padding-block: 1rem;
}

/* Bordi logici */
.nav-link {
  /* Equivalente di border-bottom in LTR */
  border-block-end: 2px solid transparent;
}

.nav-link:hover {
  border-block-end-color: var(--color-primary);
}

/* Dimensioni logiche */
.dialog-box {
  /* Equivalente di width */
  inline-size: min(500px, 90%);
  /* Equivalente di max-height */
  max-block-size: 80vh;
  /* Equivalente di overflow-y */
  overflow-block: auto;
}

/* Posizionamento logico */
.badge {
  position: absolute;
  /* Equivalente di top e right in LTR */
  inset-block-start: -0.5rem;
  inset-inline-end: -0.5rem;
}

L'adozione delle proprietà logiche rende il codice CSS intrinsecamente pronto per il supporto multilingue, eliminando la necessità di duplicare o sovrascrivere regole per i layout RTL.

View Transitions API

La View Transitions API consente di creare transizioni fluide tra diversi stati di una pagina o tra pagine diverse in un'applicazione multi-pagina (MPA). Il CSS gioca un ruolo centrale nella definizione dell'aspetto di queste transizioni.

/* Personalizzazione della transizione predefinita */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}

::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

/* Assegnazione di un nome di transizione a un elemento specifico */
.hero-image {
  view-transition-name: hero;
}

/* Transizione personalizzata per l'immagine hero */
::view-transition-old(hero) {
  animation: slide-out 0.4s ease-in-out;
}

::view-transition-new(hero) {
  animation: slide-in 0.4s ease-in-out;
}

@keyframes slide-out {
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}

@keyframes slide-in {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
}
// Attivazione della transizione tramite JavaScript
document.startViewTransition(() => {
  // Aggiornamento del DOM
  updateContent();
});

Per le applicazioni multi-pagina, è sufficiente attivare le transizioni tramite la regola CSS @view-transition senza alcun JavaScript.

/* Attivazione delle transizioni tra pagine in un'applicazione MPA */
@view-transition {
  navigation: auto;
}

La funzione light-dark()

La funzione light-dark() semplifica enormemente la gestione dei temi chiaro e scuro, consentendo di specificare entrambi i valori in un'unica dichiarazione. Richiede l'impostazione della proprietà color-scheme per funzionare correttamente.

/* Abilitazione dello schema di colori */
:root {
  color-scheme: light dark;
}

/* Definizione dei colori con un'unica dichiarazione */
body {
  /* Primo valore: tema chiaro, secondo valore: tema scuro */
  background-color: light-dark(#ffffff, #1a1a1a);
  color: light-dark(#333333, #e0e0e0);
}

.card {
  background-color: light-dark(#f5f5f5, #2d2d2d);
  border: 1px solid light-dark(#e0e0e0, #404040);
  box-shadow: 0 2px 4px light-dark(
    rgba(0, 0, 0, 0.1),
    rgba(0, 0, 0, 0.4)
  );
}

a {
  color: light-dark(#1a73e8, #8ab4f8);
}

Rispetto all'approccio tradizionale basato su prefers-color-scheme con duplicazione delle regole, light-dark() riduce drasticamente la quantità di codice necessaria e migliora la manutenibilità.

La regola @property

La regola @property permette di registrare Custom Properties con un tipo, un valore iniziale e un comportamento di ereditarietà specifici. Questo consente al browser di interpretare correttamente le Custom Properties durante le transizioni e le animazioni, cosa impossibile con le Custom Properties non tipizzate.

/* Registrazione di una proprietà tipizzata */
@property --gradient-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@property --progress {
  syntax: '<percentage>';
  initial-value: 0%;
  inherits: false;
}

/* Animazione fluida di un gradiente grazie alla proprietà tipizzata */
.animated-gradient {
  --gradient-angle: 0deg;
  background: conic-gradient(
    from var(--gradient-angle),
    #ff6b6b,
    #feca57,
    #48dbfb,
    #ff6b6b
  );
  transition: --gradient-angle 1s ease;
}

.animated-gradient:hover {
  --gradient-angle: 180deg;
}

/* Barra di progresso animabile */
.progress-ring {
  --progress: 0%;
  background: conic-gradient(
    #1a73e8 var(--progress),
    #e0e0e0 var(--progress)
  );
  border-radius: 50%;
  transition: --progress 0.8s ease-out;
}

Senza @property, il browser tratterebbe le Custom Properties come semplici stringhe, rendendo impossibile interpolarle nelle transizioni. La tipizzazione apre possibilità creative significative, soprattutto per le animazioni di gradienti e valori numerici.

Conclusione

Le caratteristiche moderne del CSS hanno trasformato il linguaggio da un semplice strumento di presentazione a una piattaforma espressiva e potente. Custom Properties, Container Queries, nesting nativo, :has(), Cascade Layers, funzioni di colore avanzate, scroll-driven animations, anchor positioning e tutte le altre funzionalità esplorate in questo articolo riducono progressivamente la dipendenza da JavaScript e dai preprocessori per compiti che appartengono naturalmente al dominio della presentazione.

L'adozione di queste funzionalità non è solo una questione di modernità stilistica: si traduce in codice più manutenibile, prestazioni migliori e componenti genuinamente riutilizzabili. Il consiglio è di iniziare a integrarle nei propri progetti, verificando il supporto dei browser attraverso risorse come Can I Use e adottando strategie di progressive enhancement dove necessario.