Comment faire un tableau HTML avec des en-têtes figés en 5 étapes
Faire un tableau HTML demande une poignée de balises, et chacun y parvient du premier coup. Le garder lisible avec cinquante lignes, c’est une autre histoire. On fixe sa hauteur, une barre de défilement surgit, puis il suffit de descendre de trois crans pour que la ligne de titres parte avec le reste. Sur deux colonnes, on devine encore. Huit colonnes de chiffres, en revanche, et on ne sait plus ce qu’on lit.
Ce tutoriel reprend, pas à pas, une méthode qui règle le problème avec du CSS et une seule petite règle JavaScript. À la fin, vous aurez un tableau dont les titres restent collés en haut, parfaitement alignés sur leurs colonnes, pendant que les données défilent dessous. Le tout sans bibliothèque de composants, sans plugin, et avec un code que vous écrivez une fois pour tous vos tableaux.
Les points essentiels à retenir
- Ce n’est pas au bloc qui affiche les titres de porter le défilement : il faut un conteneur intérieur autour de la table.
- En haut, le bloc extérieur réserve une bande avec un padding-top égal à la hauteur de la ligne de titres, à savoir 45 px dans la démonstration.
- On sort les titres du flux : chaque th reçoit une div, et c’est cette div qu’on passe en position absolute avec top 0.
- Deux lignes de JavaScript recopient la largeur de chaque th dans sa div, sans quoi les titres ne tombent pas en face des colonnes.
- Ultime réglage : avec une table large de 99,9 % plutôt que 100 %, la barre de défilement en trop disparaît.
Le point de départ : un tableau qui défile et perd ses titres
Au départ, la structure tient sur deux niveaux. Une div porte la classe du tableau ; dedans, une table des plus classiques : une ligne de th pour les titres, ensuite des lignes de td pour les données.
<div class="container_12 tableau">
<table>
<tr><th>Pays</th><th>Nombre de vues</th></tr>
<tr><td>France</td><td>124807</td></tr>
<tr><td>Maroc</td><td>43961</td></tr>
</table>
</div>
En CSS, on donne à la div une hauteur fixe et un overflow automatique. Ce duo fait apparaître la barre de défilement :
.tableau { font-size: 24px; height: 200px; margin: auto; overflow: auto; width: 450px; }
.tableau table { margin: 0; width: 100%; }
.tableau th { background: black; border: black 1px solid; color: white; padding: 10px; }
.tableau table td { background: #8C8C8C; border: black 1px solid; color: black; }
Ça fonctionne, sauf que la ligne noire des titres appartient à la zone qui défile. Comme n’importe quelle autre ligne, elle file vers le haut. Tout l’enjeu sera de l’extraire de cette zone sans casser l’alignement des colonnes.

Étape 1 : deux conteneurs plutôt qu’un, et une bande réservée en haut
Une phrase suffit pour l’idée : en haut, le bloc extérieur garde une marge intérieure vide, et les titres viendront s’afficher dans cette bande. Quant aux données, elles défilent dans un second conteneur placé entre la div de départ et la table.
- Placez une div autour de la table, par exemple avec la classe
container_tab. - Transférez la hauteur et l’overflow vers cette nouvelle div : le défilement lui revient, et non au bloc extérieur.
- Attribuez au bloc extérieur un
position: relativeafin que tout ce qu’on positionnera par la suite se cale sur son coin supérieur gauche, pas sur celui de la page. - Donnez-lui un
padding-topégal à la hauteur exacte de votre ligne de titres.
Méfiez-vous du quatrième point : c’est là que l’erreur arrive le plus vite. Ici, une marge extérieure ne sert à rien, puisque la bande doit appartenir au bloc ; il faut donc bien un padding. Sa valeur, elle, ne se devine pas. Dans la démonstration, un premier essai à 20 px laisse les titres empiéter sur la première ligne de données, puis l’inspecteur du navigateur indique 45 px pour la ligne de titres, et c’est cette valeur qu’on retient.
.tableau { font-size: 24px; margin: auto; padding-top: 45px; position: relative; width: 450px; }
.tableau .container_tab { height: 200px; overflow: auto; }

Étape 2 : une div dans chaque cellule d’en-tête
Première tentation : passer directement les th en position: absolute avec un top: 0. Essayez, vous verrez le résultat tout de suite. Les titres se placent bien dans la bande, mais ils perdent leur taille, puisqu’un élément sorti du flux ne reçoit plus la largeur que la table lui attribuait. Les deux intitulés se retrouvent collés l’un à l’autre, à mille lieues de leurs colonnes.
Pour s’en sortir, on ne touche pas au th lui-même. On place une div dans chaque cellule d’en-tête, puis c’est elle qu’on décolle :
<tr><th><div>Pays</div></th><th><div>Nombre de vues</div></th></tr>
.tableau th > div { position: absolute; top: 0; }
Pourquoi est-ce que ça marche ? Parce que ces div se positionnent par rapport au bloc extérieur, celui qui porte le position: relative, et ce bloc ne défile pas. Les lignes glissent sous les titres sans jamais les emmener. Autre effet secondaire bienvenu : les th n’ont plus rien dans leur flux, donc la zone de défilement démarre directement sur la première ligne de données.

Étape 3 : la ligne de JavaScript qui remet les titres en face des colonnes
Il reste un défaut : la div absolue ne connaît toujours pas la largeur de sa colonne. On pense évidemment à width: inherit. Mauvaise pioche. L’héritage ne fonctionne que si le th porte une largeur explicite, par exemple 50 %. Dès qu’il est en automatique, la div hérite d’une valeur automatique, autrement dit de rien du tout. Et demander à chacun de figer la largeur de toutes ses colonnes pour faire un tableau HTML correct, ce serait un bien mauvais service.
D’où ce bref passage par le JavaScript, qui tient en deux lignes grâce à jQuery :
<script type="text/javascript">
$(".tableau th").each(function(){
$(this).find("div").css("width", $(this).width())
});
</script>
La boucle passe sur chaque cellule d’en-tête, récupère la div qu’elle contient, et lui applique la largeur mesurée du th. Même idée en JavaScript natif, si jQuery n’est pas chargé sur votre page :
document.querySelectorAll(".tableau th").forEach(function (th) {
th.querySelector("div").style.width = th.getBoundingClientRect().width + "px";
});
On écrit cette règle une seule fois. Chaque tableau de votre site portant la classe en bénéficie, peu importe son nombre de colonnes.

Étape 4 : les finitions qui font la différence
Si le mécanisme tient debout, le rendu réclame encore quatre réglages. C’est grâce à eux qu’un tableau propre se distingue d’un bricolage visible.
Le fond déménage sur le conteneur
Tant que personne ne la colore, la bande réservée reste blanche, d’où un bandeau clair au-dessus d’un tableau sombre. Pour corriger, on retire le fond des titres et on le pose sur le bloc extérieur, qui couvre toute la largeur. Avec un en-tête au fond plus travaillé, une image de la hauteur de la ligne, répétée sur l’axe horizontal, fait la même chose.
Le padding, oui, mais vertical seulement
Le padding latéral des titres décale les div par rapport à leurs colonnes. On le retire sur les côtés et on garde celui du haut et du bas, qui aère le texte.
La bordure disparaît
Le léger décalage qu’on voit entre un titre et sa colonne vient de la bordure. Comme le fond est désormais uni, elle ne sert plus à rien sur les titres : on la supprime et l’alignement devient net.
99,9 % plutôt que 100 %
Une barre de défilement surnuméraire apparaît quand la table occupe 100 % de la largeur. Passer à 99,9 % la fait disparaître sans que l’œil ne voie la différence.

Le code complet du tableau à en-têtes figés
Voici l’ensemble, prêt à être recopié et adapté. Le HTML :
<div class="container_12 tableau">
<div class="container_tab">
<table>
<tr><th><div>Pays</div></th><th><div>Nombre de vues</div></th></tr>
<tr><td>France</td><td>124807</td></tr>
<tr><td>Maroc</td><td>43961</td></tr>
</table>
</div>
</div>
Le CSS :
.tableau { background: black; font-size: 24px; margin: auto; padding-top: 45px; position: relative; width: 450px; }
.tableau .container_tab { height: 200px; overflow: auto; }
.tableau table { margin: 0; width: 99.9%; }
.tableau th > div { background: black; color: white; padding: 10px 0; position: absolute; top: 0; }
.tableau table td { background: #8C8C8C; border: black 1px solid; color: black; }
Largeur, hauteur et padding reprennent les valeurs de l’exemple. Chez vous, elles dépendront de votre police et de votre mise en page : c’est le seul endroit où l’on doit mesurer avant de copier.

Les erreurs fréquentes avec les en-têtes fixes
- Laisser le défilement sur le bloc extérieur. Tant que la div qui porte les titres garde la hauteur et l’overflow, ces derniers s’en vont avec les données. Ce réglage doit descendre d’un niveau.
- Utiliser une marge au lieu d’un padding. Une marge pousse le bloc entier vers le bas, elle ne réserve aucun espace à l’intérieur. Les titres viennent alors se superposer à la première ligne.
- Positionner les th en absolute. La cellule perd alors sa taille, et l’alignement part en vrille. C’est la div intérieure qu’on doit décoller.
- Compter sur width: inherit. Elle n’agit que si la largeur du parent est écrite en dur. Sur des colonnes automatiques, rien ne se passe.
- Choisir le padding-top au jugé. Dans l’inspecteur, mesurez la hauteur réelle de la ligne de titres et reportez la valeur obtenue. Trop faible, les titres mordent sur les données ; trop grande, un vide apparaît.
- Oublier le fond de la bande. Sans couleur, la zone réservée reste blanche et trahit le montage au premier coup d’œil.
Questions fréquentes
Peut-on se passer complètement de JavaScript ?
Dans cette méthode, non, et c’est assumé. Tout le reste est du CSS, le script se contente de recopier une largeur. L’obstacle est précis : un enfant ne peut pas récupérer automatiquement la largeur de son parent quand celle-ci n’a pas été fixée. Si vous acceptez de déclarer la largeur de vos colonnes en pourcentage ou en pixels, le width: inherit redevient envisageable.
Est-ce que ça fonctionne avec plus de deux colonnes ?
Oui, et c’est même dans ce cas que la technique prend tout son sens. Comme la boucle parcourt chaque th, chaque titre obtient la largeur de sa propre colonne. Plus le tableau est large et chargé de chiffres, plus des en-têtes figés transforment la lecture.
Que se passe-t-il si la largeur du tableau change après le chargement ?
Le script recopie la largeur au moment où il s’exécute. Si la table change de dimensions après coup, par exemple parce que la fenêtre est redimensionnée ou que des lignes arrivent plus tard, on relance la même boucle pour replacer les titres face aux colonnes.
Et si mon tableau vient d’Excel ou de Word ?
Le plus simple : obtenir d’abord un code propre, ensuite appliquer la méthode décrite plus haut. Un convertisseur Excel vers HTML vous fournit la table balisée ; il ne reste qu’à y ajouter les deux conteneurs et les div dans les th. Depuis un document texte, la logique est la même avec le convertisseur Word vers HTML.
Votre tableau existe déjà dans un tableur ?
Collez-le et récupérez un code HTML propre, prêt à recevoir vos en-têtes figés.
Quiz : avez-vous tout retenu ?
1. Quel élément doit porter la hauteur et l’overflow ?
2. À quoi sert le padding-top du bloc extérieur ?
3. Pourquoi glisser une div dans chaque th ?
4. Que fait la boucle JavaScript ?
5. Pourquoi passer la table à 99,9 % de largeur ?
Désormais, vous savez faire un tableau HTML dont les titres ne s’échappent plus. Au passage, vous avez constaté que la bonne solution vient rarement du premier coup : on tente une piste, elle casse, on comprend pourquoi, puis on ajuste. Ce width: inherit qui refuse de coopérer en est la meilleure illustration. Pour des tableaux qui doivent aussi tenir sur un écran de téléphone, la suite logique est du côté des tableaux HTML responsives sans framework, où le problème de lecture se pose autrement.