Créer une vue calendrier minimaliste avec du HTML et du CSS
Si vous allez sur un profil Github (par exemple : le miens), vous verrez quelque chose qui ressemble à ça :
Pour plein de raisons j'aime bien et pas trop cette vue. Et j'avais envie d'avoir quelque chose de similaire sur mon blog… Je vous explique !
Tout ce que je vous montre ici est disponible ici :
Ce que j'aime et aime moins dans la vue contribution de Github🔗
Ce qui fait la popularité de cette vue c'est que ça permet à des gens de montrer qu'ils travaillent plus que tout le monde… J'aime pas trop ça… Ce que ça montre c'est votre activité, en grande partie basé sur votre volume de commit, ce qui ne veut rien dire ni de votre activité réelle, ni de votre apport aux projets, ni rien en fait, juste vous savez faire des git commit -m "truc" --allow-empty…
Par contre j'aime bien ce genre de graphique avec un côté prise de recul car ça permet d'avoir une vue d'ensemble d'une période assez longue (sur ma capture d'écran les 12 derniers mois, mais ça peut être aussi les 12 mois d'une année), sans prendre trop de place et si on sait qu'on ne triche pas sur l'activité qu'on a, de voir quand on a été moins bon, et peut-être prendre du recul là-dessus pour comprendre pourquoi, ensuite au choix faire en sorte de pouvoir être actif plus souvent parce que ça nous manquait à cette période, ou à l'inverse réduire son activité pour être mieux (ça c'est vous qui voyez).
Globalement, j'aime vraiment la vue d'ensemble que ça apporte qui donne une info en un coup d'œil ! Et c'est ça que j'aimerai avoir sur mon blog !
J'aimerai une vue très simple, beaucoup plus minimaliste que ce que fait Github, avec les couleurs du blog, sur chaque page archives (une page archive présente tous les articles d'une année), pour naviguer visuellement dans l'année.
Let's go HTML!🔗
Mon blog est généré statiquement donc je veux une solution purement HTML et CSS. Je passe sur la manière dont je mouline mon modèle interne pour générer le HTML, mais voici le résultat :
<section id="overview">
<ul aria-hidden="true">
<li data-previous-year="true" data-date="2025-12-29"></li><!-- Monday -->
<li data-previous-year="true" data-date="2025-12-30"></li><!-- Tuesday -->
<li data-previous-year="true" data-date="2025-12-31"></li><!-- Wednesday -->
<li data-date="2026-01-01"></li><!-- Thursday -->
<li data-date="2026-01-02"></li><!-- Friday -->
<li data-date="2026-01-03"></li><!-- Saturday -->
<li data-date="2026-01-04"></li><!-- Sunday -->
<li data-date="2026-01-05"></li>
<li data-date="2026-01-06">
<a href="#2026/01/06/revue-de-presse-janvier" title="Revue de presse - Janvier 2026"></a>
</li>
...
<li data-date="2026-03-17" data-today="true"></li>
<li data-date="2026-03-18"></li>
...
</ul>
</section>HTML
Note : j'ai ajouté les jours de la semaine sur la première semaine pour aider à comprendre.
Je veux une grille donc j'ai choisi de faire une liste non ordonnée (<ul>) ! What!? 🧐
Ça peut vous surprendre mais oui j'ai choisi de faire une liste et pas un tableau. Pourquoi ? Un tableau c'est beaucoup plus difficile à styliser graphiquement, et ça demande beaucoup plus d'effort à produire en termes de HTML. Si j'avais choisi de le faire sous la forme d'un <table> j'aurai dû penser le positionnement des dates dès la génération du HTML par exemple…
Vous noterez aussi que j'ai mis un aria-hidden="true" sur la liste. En effet j'ai choisi d'exclure cet élément des outils d'accessibilité types lecteurs d'écran. La première raison est que ça n'aurait rien apporté : j'aurais sur ma page une liste de date avec pour chaque s'il y a un article, puis la liste directe des articles… Je dois aussi admettre que je n'avais aucune idée de comment rendre tout ça accessible… Je n'ai pas encore fait le travaille d'auditer l'accessibilité de mon blog avec des outils, je m'en mordrais peut-être les doigts ce jour-là et si c'est le cas j'en reparlerais !
Dans cette liste on peut voir une série de <li> avec chacun un attribut data-date="yyyy-MM-dd", c'est simple mais ça fonctionne et vous allez voir que ça va nous servir plus tard !
On notera que les premiers ont l'attribut data-previous-year="true", car comme le nom de l'attribut l'indique : il s'agit des derniers jours de l'année précédente. Pourquoi ? Parce que je veux mettre les jours en colonne par semaine, et que la première semaine de l'année ne commence pas forcément un lundi (ou un dimanche), et que pourtant les derniers jours de l'année sont considérés comme faisant partie de la semaine 1 de l'année suivante (j'imagine pour n'avoir que des semaines complètes).
La date du jour est indiqué aussi avec un attribut spécifique data-today="true". Vous verrez que ça nous servira plus tard aussi !
Note : vous pouvez noter aussi que tous mes attributs personnalisés (et donc non standards) sont préfixés avec
data-, sans ça fonctionne mais ce n'est pas du HTML valide, qui oblige à avoir un préfixedata-, l'intérêt de ça c'est aussi que jamais vous n'aurez de conflit avec une éventuelle évolution de la spécification HTML.
Les dates où j'ai publié un article, dans l'élément <li> on retrouve un lien qui n'a pas de texte (car je n'en ai pas besoin), mais qui a une ancre comme URL et un title. Le title pour commencer indique simplement le nom de l'article, ce qui permet de l'afficher au survol (comportement natif du navigateur). L'ancre c'est moins habituel quand on ne construit pas de site web (par opposition aux logiciels de gestions / web app) : au clic sur un lien ancre, l'URL va changer pour faire apparaitre l'ancre (sans chargement / navigation), et si l'ancre correspond à un élément de la page (comprendre : si l'ancre a la même valeur que l'attribut id d'un élément de la page), le navigateur va scroll jusqu'à cet élément et l'indiquer comme "focus". On s'en servira plus tard là aussi !
Et visuellement ça donne ça :
Ça fait rêver hein ? Non ? 🤡
Note sur le thème🔗
Quitte à faire les choses biens, je vais faire une vue qui fonctionne bien en thème clair et sombre. Et je veux faire les choses proprement, donc je vais utiliser des variables CSS.
Je vous donne les variables de thème que je vais utiliser :
:root {
/* ATOMS */
--color--white: #ffffff;
--color--black: #121212;
--color--dark-gray: #333333;
--color--gray: #6c757d;
--color--light-gray: #e0e0e0;
--color--blue: #007bff;
--color--green: #28a745;
--size--quarter: 0.125rem;
--size--half: 0.25rem;
--size--1: 0.5rem;
--size--2: 1rem;
--size--3: 1.5rem;
--size--4: 2rem;
--size--5: 4rem;
/* MOLECULES */
--color--bg: light-dark(var(--color--white), var(--color--black));
--color--text: light-dark(var(--color--dark-gray), var(--color--light-gray));
--color--primary: var(--color--blue);
--color--secondary: var(--color--gray);
--color--accent: var(--color--green);
--main-text--max-width: 100ch;
--reponsive-main-text-max-width: min(
100dvw,
min(100vw, var(--main-text--max-width))
);
}CSS
Il n'y a pas de magie, c'est le thème de mon blog (avec quelques variables en moins dont je n'aurai pas du tout besoin). Pour plus de détail sur comment tout ça fonctionne, je vous renvoie vers mon article sur le style de mon blog.
J'en profite aussi pour rappeler que pour avoir proprement deux thèmes sur votre site, il suffit d'avoir <meta name="color-scheme" content="light dark"> dans le <head> de votre page.
D'une liste à une grille🔗
L'idée de base c'est d'aligner tous les éléments en colonne, sept par sept jusqu'à les avoir tous mis. De cette façon, la première colonne représente la semaine 1 de l'année, la seconde colonne la semaine 2 et ainsi de suite. Donc on veut une grille qui aura 7 lignes et entre 52 et 54 colonnes. Dans un premier temps : restons sur 53 semaines (le cas le plus courant).
Pour faire ça c'est simple, on va utiliser un display: grid associé à grid-template-columns: repeat(53, var(--overview--border-width)) pour avoir 53 colonnes, grid-template-rows: repeat(7, var(--overview--border-width)) pour avoir 7 lignes et grid-auto-flow: column pour lui dire de remplir colonne par colonne (par défaut, le layout grid remplit ligne à ligne).
#overview {
--overview--day-width: var(--size--1);
ul {
display: grid;
grid-template-columns: repeat(53, var(--overview--day-width));
grid-template-rows: repeat(7, var(--overview--day-width));
grid-auto-flow: column;
}
}CSS
Notre vue est tout de suite plus compacte !
Je ne veux pas un point pour chaque jour, je veux des cercles, avec une bordure fine. Donc pour ça j'ajoute un peu de style sur les <li> :
#overview {
--overview--month-border-color: var(--color--secondary);
--overview--day-width: var(--size--1);
--overview--day-border-radius: calc(var(--overview--day-width) / 2);
ul {
li {
border: 1px solid var(--overview--month-border-color);
border-radius: var(--overview--day-border-radius);
width: var(--overview--day-width);
height: var(--overview--day-width);
padding: 0;
margin: 0;
}
}
}CSS
C'est un peu mieux, mais il y a toujours les puces des différents <li> et on a aussi le padding-left natif des listes (c'est ça qui permet à la fois de laisser de la place pour les puces et que les listes soient de base un peu indentée par rapport au reste du texte). Corrigeons ça ! Et au passage on va ajouter un peu d'espace entre les cercles pour que ce ne soit plus tout tassé !
#overview {
--overview--padding: var(--size--half);
ul {
list-style-type: none;
gap: 1px;
padding: var(--overview--padding);
}
}CSS
On a notre grille propre. Il ne reste plus qu'à mettre en avant les éléments :
- masquer les jours de l'année précédente en effaçant la bordure les
<li>avec l'attributdata-previous-year; - ajouter un fond pour les
<li>qui contiennent un élément<a>(pour mettre en évidence les jours avec un article) ; - ajouter une bordure colorée pour la date du jour (les
<li>qui ont l'attributdata-today) ;
Pour faire tout ça on va utiliser des sélecteurs sur attribut (li[data-today] par exemple) et le pseudo-sélecteur :has() (:has(a) par exemple).
Je vais aussi en profiter pour faire en sorte que le lien ancre vers l'article prenne toute la place dans le cercle, ça va permettre de cliquer n'importe où dans le cercle pour accéder à l'ancre.
#overview {
--overview--today-border-color: var(--color--accent);
--overview--with-article-color: var(--color--primary);
ul {
li {
&[data-previous-year] {
border: none;
}
&[data-today] {
border-color: var(--overview--today-border-color);
border-width: 2px;
}
&:has(a) {
background-color: var(--overview--with-article-color);
a {
display: block;
width: 100%;
height: 100%;
}
}
}
}
}CSS
On peut encore faire mieux !
Et si on colorait différemment la bordure des mois pair ? Sans ajouter quoi que ce soit au HTML évidemment ! On va utiliser un sélecteur d'attribut qui se base sur sa valeur : li[data-date*="-02-"] signifie "tous les <li> qui ont l'attribut data-date avec une valeur contenant (*=) la chaîne de caractère -02-", autrement dit pour nous "tous les <li> de février. Et donc il suffit de faire pareil pour les autres mois.
On va aussi utiliser une fonctionnalité récente de CSS : les couleurs relatives. L'idée c'est de pouvoir prendre la valeur d'une couleur et changer juste une partie des valeurs, dans mon cas je vais prendre la couleur --overview--month-border-color et réduire de 30% son opacité en passant le canal alpha à 0.7, ça permet que la couleur s'adapte au thème automatiquement sans me casser la tête.
Au passage je renomme --overview--month-border-color en --overview--odd-month-border-color pour être plus logique.
#overview {
--overview--odd-month-border-color: var(--color--secondary);
--overview--even-month-border-color: rgb(from var(--overview--odd-month-border-color) r g b / 0.7);
ul {
li {
border: 1px solid var(--overview--odd-month-border-color);
&[data-date*="-02-"], &[data-date*="-04-"],
&[data-date*="-06-"], &[data-date*="-08-"],
&[data-date*="-10-"], &[data-date*="-12-"] {
border-color: var(--overview--even-month-border-color);
}
}
}
}CSS
Comme on connaît la date du jour, on pourrait aussi rendre moins visible les jours des dates dans le futur. Toujours sans rien ajouter au HTML évidemment. Facile avec le combinateur "frère suivant" (Subsequent-sibling Combinator en anglais) : li[data-today] ~ li va indiquer qu'on veut sélectionner tous les <li> qui suivent un <li data-today>.
#overview {
ul {
li[data-today] ~ li {
opacity: 0.5;
}
}
}CSS
Facile non ?
52 à 54 semaines par an…🔗
Il manque un dernier truc : comment on gère le fait d'avoir entre 52 et 54 semaines ? Et dans quels cas ça arrive ?
Je vous ai mis un article qui détaille tout ça dans les sources mais pour résumé : je suis français, je suis donc dans une zone qui fonctionne en ISO 8601, donc je vais me baser là-dessus (en Amérique du Nord ce serait différent par exemple).
Pour faire un résumé grossier :
- mathématiquement : une année c'est 52 semaines (7 jours x 52 semaines = 364 jours) + 1 à 2 jours (2j si c'est une année bissextile) ;
- mais comme les années ne sont pas sur un nombre de jour divisible par 7, les jours de la première semaine de l'année se décale d'année en année et la dernière semaine de l'année est souvent la même semaine que la première de l'année suivante ;
- la règle est simple : si le 1er janvier tombe entre le lundi et jeudi inclus, les derniers jours de l'année N-1 et les premiers de l'année N sont comptés comme la semaine 1, si le 1er janvier tombe entre le vendredi et le dimanche inclus, ces mêmes jours sont la dernière semaine de l'année N-1 (donc semaine 53) ;
- en cas d'année bissextile où le 1er janvier est un dimanche (dernier jour de la semaine en ISO), alors le 31 décembre sera un lundi (premier jour de la semaine en ISO), on aura donc 54 semaines à afficher ;
Note : je vous laisse regarder plus en détail le site que j'ai mis en source, mais il n'y a qu'en notation américaine qu'on aura une semaine numérotée 54, pas en calendrier ISO.
Bon et du coup, pour nous on fait comment ? On fait des calculs savants pour gérer ça ? 🤔
Plus simple : on se pose pour voir le problème autrement et on laisse CSS se débrouiller. 😇
Le cas 52 semaines à afficher n'arrivera jamais à cause du nombre de jour dans l'année, sur une année de 365 jours, les cas les plus extrêmes ce sera 52 semaines pleines et une journée toute seule soit au début (comme 2026, le 1er janvier est un dimanche, tout seul sur sa semaine) soit à la fin (comme en 2028, où le 1er janvier est un lundi, et le 31 décembre un lundi aussi). 53 semaines sera notre cas par défaut le plus courant.
Et de temps en temps, on aura une 54ème semaine qui va apparaitre. Quand on aura 366 jours dans l'année uniquement, et dans le cas où le premier jour de l'année et le dernier jours de l'année sont seuls sur leur semaine. En d'autres mots : on aura 366 jours affichés + 6 jours de l'année précédente qui sont masqués mais sont là pour l'alignement ; donc on aura 54 semaines si on a 372 <li> dans notre liste <ul>, et ça c'est facile à gérer en CSS !
#overview {
ul {
&:has(li:nth-child(372)) {
grid-template-columns: repeat(54, var(--overview--day-width));
}
}
}CSS
Mettre en avant l'élément sélectionné🔗
Je l'expliquais un peu plus haut : quand on clique sur un de mes liens dans la vue calendrier, on va créer une ancre dans l'URL :
Eh bien en CSS l'élément qui possède l'id de même valeur pourra être sélectionné avec le pseudo-sélecteur :target.
Donc je peux faire une mise en avant toute simple avec :
#previous-posts {
ul {
li {
a {
&:target {
color: var(--color--primary)
}
}
}
}
}CSS
Conclusion🔗
À la base quand j'ai ajouté ça à mon blog, je ne pensais pas en faire un article : je me suis amusé à ajouter ça, puis j'ai pris un pas de recul en me disant que y'avait des trucs intéressants à montrer et expliquer ! Surtout à l'heure des LLMs qui crachent du Tailwind les yeux fermés à longueur de temps alors qu'on pourrait faire tellement plus simple en CSS… Mais bref !
On aurait pu faire des choses très différentes visuellement, même en se basant sur des mécaniques très similaires côté sélecteurs. Mais là on parle d'une question de goût / choix. J'ai choisi de faire quelque chose de très simple et minimaliste parce que c'est ça que j'avais envie de faire !
À noter que j'ai une particularité sur le blog : je compte mes années de mars à février, donc j'ai décalé mes archives pour caler à ce cycle. Maintenant : ça ne change rien à tout ce que j'ai décrit dans cet article, simplement, ne comparez pas les jours exacts avec mon blog, vous allez vous arracher les cheveux !
Sources :
- Démonstration interactive
- Code source de la démo
- Combien de semaines dans une année : 52 ou 53 selon le calendrier
- CSS Combinators (w3schools)
- Using relative colors (mdn)
Crédit photo : Générée via Mistral AI avec le prompt suivant :
A cozy wooden desk bathed in soft golden light, reminiscent of a late autumn afternoon. The centerpiece is a large wooden calendar, intricately carved with circular reliefs representing each day of the year. Some circles are illuminated in soft blue or green, symbolizing published articles. A red panda, sitting on a cushion in the bottom right corner (occupying less than 1/3 of the height), is gently painting one of the calendar circles with a fine brush. The red panda is slightly turned in a 3/4 back view, surrounded by a subtle halo of light and 8-10 floating maple leaves in shades of red, orange, and yellow. Around the calendar, faint holograms display snippets of CSS/HTML code and dates, blending seamlessly into the scene. The atmosphere is warm, poetic, and dreamlike, inspired by Studio Ghibli's style, with a harmonious mix of organic textures (wood, fabric) and subtle high-tech elements (holograms with wooden frames). The overall mood is immersive, cozy, and magical, evoking a sense of timeless creativity and reflection.