📌 Record grid con CSS Grid: alternativa alle tabelle

Molti layout “a tabella” non sono tabelle dati: sono elenchi di record (utenti, ordini, ticket) mostrati a colonne per comodità. In questo articolo costruisci un record grid moderno con CSS Grid, responsive tramite container queries, con CSS isolato e senza fingere semantica tabellare.

Due monitor mostrano codice sorgente e una griglia di record con immagini, nomi, stati e azioni.

Autore: Redazione · Creato: 17/02/2026 22:12

Record grid con CSS Grid: perché NON è una tabella

Una tabella HTML (<table>) è la scelta corretta quando i dati sono intrinsecamente tabellari: relazioni riga/colonna, intestazioni che governano celle, lettura matriciale. Ma molti layout “a tabella” sul web non sono tabelle: sono elenchi di record (utenti, ordini, ticket, prodotti) presentati a colonne per comodità visiva.

In questi casi, forzare <table> è spesso più fragile (soprattutto su mobile) e meno modulare. La soluzione ingegneristicamente pulita è un record grid: una lista di record resa “tabellare” dal CSS, senza fingere semantica tabellare.

Regola operativa (decisione rapida)

  • Usa <table> se le intestazioni descrivono colonne/celle e la lettura corretta è riga/colonna.
  • Usa record grid se ogni riga è un oggetto (record) e le “colonne” sono solo attributi mostrati in modo comodo.
  • Non usare ARIA grid solo per “fare tabella”: un grid ARIA è un widget interattivo, non un layout statico.

Obiettivo tecnico

Costruire un componente riusabile che:

  • mostra un elenco di record in colonne allineate su contenitori larghi;
  • diventa cards su contenitori stretti, senza perdere leggibilità;
  • usa CSS Grid come motore principale (allineamento bidimensionale);
  • usa container queries per rispondere al contenitore (perfetto per CMS e layout a colonne);
  • isola completamente lo stile dentro un id (zero side-effect sul sito).

1) Markup: record grid senza classi

Il markup sotto è intenzionalmente “semplice”: un wrapper con id, un header visivo (non semantica tabellare), e un body con righe. Ogni “cella” ha un data-label usato per mostrare l’etichetta in vista mobile.

<h3>Demo: elenco utenti</h3>
<p>Layout “tipo tabella” per record (non tabella dati).</p>

<div id="ap-rg" aria-label="Elenco record">
  <div id="ap-rg-head" aria-hidden="true">
    <div>Nome</div>
    <div>Ruolo</div>
    <div>Stato</div>
    <div>Ultimo accesso</div>
  </div>

  <div id="ap-rg-body">
    <div data-rg-row>
      <div data-label="Nome"><strong>Mario Rossi</strong></div>
      <div data-label="Ruolo">DevOps</div>
      <div data-label="Stato"><span data-status="ok">Attivo</span></div>
      <div data-label="Ultimo accesso">2026-02-10</div>
    </div>

    <div data-rg-row>
      <div data-label="Nome"><strong>Amin Haddad</strong></div>
      <div data-label="Ruolo">Designer</div>
      <div data-label="Stato"><span data-status="warn">In revisione</span></div>
      <div data-label="Ultimo accesso">2026-02-14</div>
    </div>

    <div data-rg-row>
      <div data-label="Nome"><strong>Laura Bianchi</strong></div>
      <div data-label="Ruolo">Support</div>
      <div data-label="Stato"><span data-status="off">Sospeso</span></div>
      <div data-label="Ultimo accesso">2026-01-29</div>
    </div>
  </div>
</div>

2) CSS: Grid “vero” + isolamento totale

Qui avviene il salto di livello: niente CSS globale, niente classi, nessun impatto su body o tipografia del sito. Tutto vive dentro #ap-rg. La griglia è definita con grid-template-columns + minmax() per evitare colonne ingestibili.

/* ===== Record grid: stile isolato + contrasto alto ===== */
#ap-rg{
  /* Palette (light default) */
  --rg-bg: #ffffff;
  --rg-text: #111827; /* quasi-nero leggibile */
  --rg-muted: #4b5563; /* etichette / meta */
  --rg-border: rgba(17,24,39,.18);

  --rg-head-bg: #0f172a; /* header “deciso” */
  --rg-head-text: #ffffff;

  --rg-row-even: rgba(15,23,42,.04);
  --rg-row-hover: rgba(15,23,42,.08);

  --rg-radius: 14px;
  --rg-shadow: 0 10px 28px rgba(0,0,0,.10);

  background: var(--rg-bg);
  color: var(--rg-text);
  border: 1px solid var(--rg-border);
  border-radius: var(--rg-radius);
  box-shadow: var(--rg-shadow);
  overflow: hidden;

  /* Container queries */
  container-type: inline-size;
  container-name: ap-rg;

  /* Aiuta UA per form controls se presenti */
  color-scheme: light dark;
}

/* Dark mode (solo dentro il componente) */
@media (prefers-color-scheme: dark){
  #ap-rg{
    --rg-bg: #0b0f14;
    --rg-text: #e5e7eb;
    --rg-muted: #9ca3af;
    --rg-border: rgba(229,231,235,.16);

    --rg-head-bg: #111827;
    --rg-head-text: #f9fafb;

    --rg-row-even: rgba(255,255,255,.03);
    --rg-row-hover: rgba(255,255,255,.06);

    --rg-shadow: 0 14px 34px rgba(0,0,0,.45);
  }
}

/* Header visivo */
#ap-rg-head{
  display: grid;
  grid-template-columns:
    minmax(14ch, 1.4fr)
    minmax(10ch, 1fr)
    minmax(10ch, .9fr)
    minmax(12ch, .9fr);
  gap: 10px;

  padding: 12px 14px;
  font-weight: 800;
  letter-spacing: .2px;

  background: var(--rg-head-bg);
  color: var(--rg-head-text);
  border-bottom: 1px solid rgba(0,0,0,.15);
}

/* Body */
#ap-rg-body{
  display: block;
}

/* Riga record */
#ap-rg-body > [data-rg-row]{
  display: grid;
  grid-template-columns:
    minmax(14ch, 1.4fr)
    minmax(10ch, 1fr)
    minmax(10ch, .9fr)
    minmax(12ch, .9fr);
  gap: 10px;

  padding: 12px 14px;
  border-bottom: 1px solid rgba(0,0,0,.08);
}

/* Zebra + hover più “visibili” */
#ap-rg-body > [data-rg-row]:nth-child(even){
  background: var(--rg-row-even);
}
#ap-rg-body > [data-rg-row]:hover{
  background: var(--rg-row-hover);
}

/* Celle: robustezza su testi lunghi */
#ap-rg [data-rg-row] > div{
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Badge stato: colori leggibili (con testo coerente) */
#ap-rg [data-status]{
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-weight: 800;
  font-size: 0.95em;

  border: 1px solid rgba(0,0,0,.18);
  background: rgba(0,0,0,.06);
  color: var(--rg-text);
}

#ap-rg [data-status="ok"]{
  background: #dcfce7;
  border-color: #22c55e;
  color: #14532d;
}
#ap-rg [data-status="warn"]{
  background: #fef3c7;
  border-color: #f59e0b;
  color: #78350f;
}
#ap-rg [data-status="off"]{
  background: #fee2e2;
  border-color: #ef4444;
  color: #7f1d1d;
}

/* Badge in dark mode: stessi significati, luminanza compatibile */
@media (prefers-color-scheme: dark){
  #ap-rg [data-status]{
    border-color: rgba(255,255,255,.18);
    background: rgba(255,255,255,.06);
    color: var(--rg-text);
  }

  #ap-rg [data-status="ok"]{
    background: rgba(34,197,94,.18);
    border-color: rgba(34,197,94,.55);
    color: #bbf7d0;
  }
  #ap-rg [data-status="warn"]{
    background: rgba(245,158,11,.18);
    border-color: rgba(245,158,11,.55);
    color: #fde68a;
  }
  #ap-rg [data-status="off"]{
    background: rgba(239,68,68,.18);
    border-color: rgba(239,68,68,.55);
    color: #fecaca;
  }
}

/* ===== Container queries: vista stretta = cards ===== */
@container ap-rg (max-width: 520px){
  #ap-rg-head{ display: none; }

  #ap-rg-body > [data-rg-row]{
    grid-template-columns: 1fr;
    gap: 8px;
  }

  /* Etichetta in card-view: più chiara e “separata” */
  #ap-rg [data-rg-row] > div::before{
    content: attr(data-label) ": ";
    font-weight: 800;
    color: var(--rg-muted);
  }
}

/* ===== Fallback senza container queries ===== */
@supports not (container-type: inline-size){
  @media (max-width: 520px){
    #ap-rg-head{ display: none; }

    #ap-rg-body > [data-rg-row]{
      grid-template-columns: 1fr;
      gap: 8px;
    }

    #ap-rg [data-rg-row] > div::before{
      content: attr(data-label) ": ";
      font-weight: 800;
      color: var(--rg-muted);
    }
  }
}

3) Perché questa soluzione è “superiore” (ingegneria, non estetica)

  • Allineamento reale: CSS Grid gestisce righe e colonne come struttura bidimensionale, senza hack di Flexbox.
  • Componente riusabile: con container queries non dipendi dal viewport; il componente si adatta a dove lo incolli (colonne, box, sidebar).
  • CSS isolato: nessuna guerra col tema del sito, nessun leak su body o elementi esterni.
  • Mobile leggibile: da colonne a cards con etichette automatiche (data-label).

4) Errori tipici da evitare

  • Chiamarla “tabella” e poi usarla per dati tabellari veri: se i dati sono matriciali, usa <table>.
  • Usare role="grid" per “dare semantica”: un grid ARIA implica navigazione e focus da widget. Se non implementi quel comportamento, peggiori l’accessibilità invece di migliorarla.
  • CSS globale nel tutorial (es. body { ... }): in un CMS significa contaminazione e bug laterali.
  • Colonne senza minmax(): con testi reali, prima o poi esplode un layout (overflow, salti, wrapping illogico).

5) Opzionale: sorting leggero (solo se serve davvero)

Se vuoi trasformare questo record grid in un widget “quasi-tabellare” (ordinamento), fallo senza ARIA grid: aggiungi due bottoni esterni (o link) che ordinano il DOM delle righe. Esempio minimale: ordina per “Nome” o “Ultimo accesso”.

<p>
  <button type="button" id="ap-rg-sort-name">Ordina per Nome</button>
  <button type="button" id="ap-rg-sort-date">Ordina per Ultimo accesso</button>
</p>
(function(){
  var body = document.getElementById("ap-rg-body");
  if(!body) return;

  function getRows(){
    return Array.prototype.slice.call(body.querySelectorAll("[data-rg-row]"));
  }

  function sortBy(colIndex, isDate){
    var rows = getRows();

    rows.sort(function(a,b){
      var av = a.children[colIndex] ? a.children[colIndex].textContent.trim() : "";
      var bv = b.children[colIndex] ? b.children[colIndex].textContent.trim() : "";

      if(isDate){
        return new Date(bv) - new Date(av); // discendente
      }
      return av.localeCompare(bv, "it", { sensitivity: "base" });
    });

    rows.forEach(function(r){ body.appendChild(r); });
  }

  var btnName = document.getElementById("ap-rg-sort-name");
  var btnDate = document.getElementById("ap-rg-sort-date");

  if(btnName) btnName.addEventListener("click", function(){ sortBy(0, false); });
  if(btnDate) btnDate.addEventListener("click", function(){ sortBy(3, true); });
})();

Demo: elenco utenti

Layout “tipo tabella” per record (non tabella dati).

Mario Rossi
DevOps
Attivo
2026-02-10
Amin Haddad
Designer
In revisione
2026-02-14
Laura Bianchi
Support
Sospeso
2026-01-29
Messaggio sponsorizzato
Pubblicità

🔗 Condividi l'articolo:

Autore: Redazione · Creato: 17/02/2026 22:12 · Ultima modifica: 03/09/2026 12:19