Tailwind CSS : exercices
Bloc Laravel · séances 6 et 7 · ~3 h
Au chapitre 13, vous avez vu les classes utilitaires, @vite, @theme et les composants Blade à variantes. Dans ce chapitre, vous mettez en forme un site entier, celui de la ludothèque des Tilleuls. Le site fonctionne déjà : il affiche un catalogue de jeux de société, la fiche de chaque jeu, la liste des prêts et un formulaire d'ajout. Ses vues ne contiennent pourtant aucune classe, et son layout ne charge aucun CSS.
À la fin de ce chapitre, vous serez capables de :
- brancher Tailwind dans un layout, et poser un conteneur commun à toutes les pages,
- construire des composants Blade à variantes avec
@classet$attributes->class(), - adapter une grille, un tableau et un menu à la largeur de l'écran,
- placer le JavaScript d'une page dans
resources/js/app.js, et décrire l'état d'un menu avecaria-expanded, - reproduire un écran d'après une capture, et vérifier le résultat avec une liste de critères.
1. Le site de la ludothèque
Installer le dépôt (10 minutes, une seule fois)
Sur github.com/opmvpc/tailwind-26, créez votre copie avec le bouton Fork. Ouvrez ensuite un terminal dans le dossier de Herd ou de Laragon, et préparez le projet :
git clone https://github.com/VOTRE-COMPTE/tailwind-26.git
cd tailwind-26
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
npm install
Les cinq premières commandes sont celles du dépôt eloquent-26, au chapitre 10. Avec --seed, migrate remplit aussi la base : douze jeux, huit membres et quinze prêts. Les dates des prêts partent du jour où vous lancez la commande, et vos dates diffèrent donc de celles des captures. npm install recrée node_modules/, le dossier des paquets JavaScript, dont Vite et Tailwind.
Lancez ensuite cette commande dans un terminal, et laissez-le ouvert pendant tout le travail :
composer run dev
Comme au chapitre 13, composer run dev lance npm run dev : chaque nouvelle classe reçoit sa règle CSS dès que vous enregistrez la vue. Avec Herd, ouvrez http://tailwind-26.test. Avec Laragon, rechargez-le d'abord depuis son menu.
Les pages
| Adresse | Vue | Ce qu'elle affiche |
|---|---|---|
/jeux |
games/index.blade.php |
le catalogue, avec la couverture de chaque jeu |
/jeux/{id} |
games/show.blade.php |
la fiche d'un jeu et l'historique de ses prêts |
/jeux/nouveau |
games/create.blade.php |
le formulaire d'ajout d'un jeu |
/prets |
loans/index.blade.php |
tous les prêts, dans un tableau |
/composants |
styleguide.blade.php |
les boutons, les statuts et la carte du site |
La page /composants rassemble chaque élément de l'interface, dans toutes ses variantes. Une page de ce genre s'appelle un guide de style, styleguide en anglais. Vous y vérifiez un composant sans chercher une page qui l'affiche dans le bon état.
Les routes, les contrôleurs et les modèles ne changent pas. Vous ne modifiez que des fichiers de resources/ : les vues, css/app.css et js/app.js. Le projet utilise la teinte zinc, un gris très légèrement bleuté, à la place du gray du chapitre 13. Ses nuances vont aussi de 50 à 950.
Lire un exercice
Les exercices 14.1 et 14.2 donnent tout le code, et expliquent chaque classe. Les exercices 14.3 à 14.5 donnent une capture de l'écran fini, une banque de classes et des critères à cocher. Chaque banque contient les classes du corrigé dans le désordre, et trois intrus qui ne servent pas dans l'exercice. Les exercices 14.6 à 14.8 ne donnent que la capture et les critères.
Chaque critère se vérifie à l'écran :
- une largeur : ouvrez les outils de développement avec F12, puis le mode d'affichage mobile avec Ctrl+Maj+M, et tapez la largeur voulue,
- le clavier : cliquez dans la barre d'adresse, puis avancez d'un lien ou d'un bouton au suivant avec la touche Tab,
- un attribut : dans les outils de développement, l'onglet Éléments affiche le HTML de la page tel qu'il est en ce moment, attributs compris.
Le corrigé de chaque exercice se cache sous un bouton. Ouvrez-le après votre essai, et comparez avec votre code.
2. Pas à pas : le cadre et le bouton
14.1 Brancher Tailwind et poser le cadre (25 minutes)
Charger le CSS. Ouvrez resources/views/components/layouts/app.blade.php. Ajoutez deux lignes à la fin du <head>, sous <title> :
<title>{{ $title }} · Ludothèque des Tilleuls</title>
@fonts
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
Rechargez /jeux. Avant toute classe, la page a déjà changé. Le titre « Catalogue » a la taille du texte courant, les listes n'ont plus de puces, et les liens ne sont plus ni bleus ni soulignés. @import 'tailwindcss' apporte en effet la remise à zéro des styles du navigateur, vue au chapitre 13. Chaque élément ne prend plus que le style de ses classes.
La couleur de marque. La ludothèque a choisi un bleu canard. Ajoutez onze variables de thème dans le bloc @theme de resources/css/app.css, sous --font-sans :
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
/* La couleur de la ludothèque : un bleu canard, de 50 (très clair) à 950 (très foncé). */
--color-brand-50: oklch(0.98 0.016 195);
--color-brand-100: oklch(0.95 0.04 195);
--color-brand-200: oklch(0.9 0.07 195);
--color-brand-300: oklch(0.83 0.1 195);
--color-brand-400: oklch(0.74 0.12 195);
--color-brand-500: oklch(0.64 0.12 196);
--color-brand-600: oklch(0.54 0.105 198);
--color-brand-700: oklch(0.46 0.09 200);
--color-brand-800: oklch(0.4 0.075 201);
--color-brand-900: oklch(0.35 0.06 202);
--color-brand-950: oklch(0.25 0.045 204);
}
Comme au chapitre 13, chaque variable --color-brand-… crée toutes les classes de sa nuance : bg-brand-600, text-brand-700, outline-brand-600, et les autres. La fonction CSS oklch() reçoit trois nombres : la clarté, de 0 à 1, l'intensité de la couleur, et la teinte, un angle sur le cercle des couleurs. D'une nuance à l'autre, la teinte reste proche de 195 et la clarté baisse. Les couleurs de Tailwind sont écrites de la même façon.
Le corps de la page. Dans le layout, remplacez la balise <body>, la balise <main> et le pied de page :
<body class="flex min-h-screen flex-col bg-zinc-50 font-sans text-zinc-900 antialiased">
…
<main class="mx-auto w-full max-w-6xl flex-1 px-4 py-8 sm:px-6 sm:py-12 lg:px-8">
{{ $slot }}
</main>
<footer class="border-t border-zinc-200">
<div class="mx-auto flex max-w-6xl flex-col gap-2 px-4 py-6 text-sm text-zinc-500 sm:flex-row sm:justify-between sm:px-6 lg:px-8">
<p>Ludothèque des Tilleuls, rue des Tilleuls 12, 1300 Wavre. Ouverte le mercredi et le samedi, de 10 h à 17 h.</p>
<p><a href="{{ route('styleguide') }}" class="underline underline-offset-4 hover:text-zinc-900">Les composants du site</a></p>
</div>
</footer>
</body>
flex min-h-screen flex-colsur<body>range l'en-tête,<main>et le pied de page en colonne, sur au moins la hauteur de la fenêtre.flex-1donne à<main>toute la hauteur restante. Le pied de page reste donc en bas de la fenêtre, même sur une page courte.mx-auto max-w-6xlcentre un bloc de 72 rem de large au plus. Dans une colonne Flex, un enfant aux margesautone prend que la largeur de son contenu :w-fulllui rend toute la largeur, quemax-w-6xllimite ensuite.px-4 sm:px-6 lg:px-8élargit la marge intérieure avec l'écran : 16 pixels sur un téléphone, 24 à partir de 640 pixels, 32 à partir de 1 024.flex-col sm:flex-row sm:justify-betweenplace les deux paragraphes du pied de page l'un sous l'autre, puis côte à côte à partir de 640 pixels.font-sansapplique la police Instrument Sans de@theme, etantialiasedaffine le dessin des lettres sur macOS.
L'en-tête. Remplacez la balise <header> et le lien du logo. Gardez la balise <nav> telle quelle :
<header class="border-b border-zinc-200 bg-white">
<div class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-6 gap-y-2 px-4 py-3 sm:px-6 lg:px-8">
<a href="{{ route('games.index') }}" class="flex items-center gap-2 rounded-md text-lg font-semibold tracking-tight focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-600">
<span class="grid size-8 place-items-center rounded-lg bg-brand-600 text-sm font-semibold text-white" aria-hidden="true">LT</span>
Ludothèque des Tilleuls
</a>
<nav aria-label="Navigation principale">
…
</nav>
</div>
</header>
Le <div> de l'en-tête reprend mx-auto max-w-6xl et les mêmes marges que <main> et le pied de page : le logo, le titre et l'adresse commencent à la même distance du bord. grid size-8 place-items-center dessine un carré de 32 pixels, dont place-items-center centre le contenu. aria-hidden="true" cache « LT » aux lecteurs d'écran, qui ne lisent que le nom de la ludothèque.
Le titre de chaque page. Dans games/index.blade.php, entourez le titre et sa phrase d'un <div> :
<div class="mb-8">
<h1 class="text-3xl font-semibold tracking-tight">Catalogue</h1>
<p class="mt-2 text-zinc-600">{{ $games->count() }} jeux à emprunter pour trois semaines.</p>
</div>
Faites de même dans loans/index.blade.php, games/create.blade.php et styleguide.blade.php. Donnez aussi text-zinc-600 aux paragraphes « Aucun jeu dans le catalogue. » et « Aucun prêt pour l'instant. ». Les titres sont en font-semibold, car le projet télécharge Instrument Sans en 400, 500 et 600, et non en 700.

Le menu reste une liste verticale à droite du logo, et les jeux restent empilés. Ils prennent leur forme aux exercices 14.5 et 14.3.
Critères
- Le fond de la page est gris très clair, et l'en-tête blanc est séparé du contenu par un trait.
- À 1 280 pixels, le logo, le titre de la page et l'adresse du pied de page commencent à la même distance du bord gauche.
- Sur
/composants, une page courte, le pied de page touche le bas de la fenêtre. - À 375 pixels, les deux paragraphes du pied de page sont l'un sous l'autre, et côte à côte à 640 pixels.
- Au clavier, le logo reçoit un contour bleu canard, écarté du texte.
14.2 Le bouton et ses variantes (20 minutes)
La page /composants affiche quatre boutons sans style : enregistrer, annuler, supprimer, et un bouton désactivé. Créez le composant :
php artisan make:component button --view
INFO View [resources/views/components/button.blade.php] created successfully.
La base. Remplacez tout le contenu du fichier créé :
@props(['variant' => 'primary'])
<button {{ $attributes->merge(['type' => 'button'])->class([
'inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold shadow-xs transition-colors',
]) }}>
{{ $slot }}
</button>
$attributes->merge(['type' => 'button'])donne au bouton le typebuttonpar défaut. Sans attributtype, un<button>placé dans un formulaire l'envoie. Untypeécrit dans la balise<x-button>remplace cette valeur.->class([...])reçoit un tableau comme@class, et ajoute les classes écrites dans la balise<x-button>. Le chapitre 13 écrivaitclass()avantmerge(): l'ordre ne change pas le résultat.inline-flex items-center justify-center gap-2centre le texte du bouton, et l'écarte d'une icône s'il en reçoit une.transition-colorsfait passer les couleurs d'un état à l'autre en 150 millisecondes, au lieu d'un changement brusque.
Les variantes et les états. Complétez le tableau :
@props(['variant' => 'primary'])
<button {{ $attributes->merge(['type' => 'button'])->class([
'inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold shadow-xs transition-colors',
'focus-visible:outline-2 focus-visible:outline-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
'bg-brand-600 text-white hover:bg-brand-700 focus-visible:outline-brand-600 disabled:hover:bg-brand-600' => $variant === 'primary',
'bg-white text-zinc-900 ring-1 ring-zinc-300 ring-inset hover:bg-zinc-50 focus-visible:outline-zinc-500 disabled:hover:bg-white' => $variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600 disabled:hover:bg-red-600' => $variant === 'danger',
]) }}>
{{ $slot }}
</button>
| Ligne | Ce qu'elle fait |
|---|---|
focus-visible:outline-2 focus-visible:outline-offset-2 |
au focus du clavier, un contour de 2 pixels, écarté du bouton de 2 pixels |
disabled:cursor-not-allowed disabled:opacity-50 |
un bouton désactivé devient à moitié transparent, et le curseur montre un sens interdit |
focus-visible:outline-brand-600, -zinc-500, -red-600 |
la couleur du contour, une par variante |
ring-1 ring-zinc-300 ring-inset |
le bord du bouton secondaire : un contour de 1 pixel, dessiné à l'intérieur du bouton |
disabled:hover:bg-brand-600 |
au survol d'un bouton désactivé, le fond reste celui du repos |
Les préfixes s'additionnent, comme md:hover: au chapitre 13. Sans disabled:hover:bg-brand-600, un bouton désactivé foncerait au survol, comme s'il répondait au clic.
Le guide de style. Dans styleguide.blade.php, sous le <div> du titre, regroupez les trois sections dans un <div>, et remplacez les quatre boutons :
<div class="space-y-10">
<section>
<h2 class="text-xl font-semibold tracking-tight">Boutons</h2>
<p class="mt-4 flex flex-wrap gap-3">
<x-button>Enregistrer</x-button>
<x-button variant="secondary">Annuler</x-button>
<x-button variant="danger">Supprimer</x-button>
<x-button disabled>Indisponible</x-button>
</p>
</section>
…
</div>
Donnez les classes du titre « Boutons » aux deux autres titres <h2>. Affichez ensuite le code source de la page avec Ctrl+U :
<button class="inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold shadow-xs transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50 bg-brand-600 text-white hover:bg-brand-700 focus-visible:outline-brand-600 disabled:hover:bg-brand-600" type="button">
Enregistrer
</button>
…
<button class="inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold shadow-xs transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50 bg-brand-600 text-white hover:bg-brand-700 focus-visible:outline-brand-600 disabled:hover:bg-brand-600" type="button" disabled="disabled">
Indisponible
</button>
@class a gardé les trois premières chaînes et celle de la variante choisie. Le quatrième bouton n'a pas d'attribut variant : il prend primary, la valeur par défaut de @props.
Le formulaire. Dans games/create.blade.php, remplacez le bouton d'envoi :
<x-button type="submit">Ajouter le jeu</x-button>
Le type="submit" écrit dans la balise remplace le type="button" de merge(), et le bouton envoie toujours le formulaire.

Critères, sur /composants :
- Les quatre boutons sont bleu canard, blanc bordé de gris, rouge, et bleu canard pâle.
- Au survol, les trois premiers foncent progressivement. Le quatrième ne change pas, et le curseur montre un sens interdit.
- Au clavier, chaque bouton actif reçoit un contour de sa couleur, écarté du bouton. Le bouton désactivé ne reçoit pas le focus.
- Dans le code source, les quatre boutons portent
type="button", et celui de/jeux/nouveauportetype="submit". - Le formulaire de
/jeux/nouveaus'envoie toujours : un clic sur « Ajouter le jeu », avec les champs remplis, affiche la fiche du nouveau jeu.
3. Avec une banque de classes : la carte, le tableau, le menu
14.3 La carte et la grille du catalogue (25 minutes)
Chaque jeu du catalogue s'affiche dans une carte : la couverture en haut, puis la catégorie, le titre et une ligne d'informations. La carte du guide de style a le même cadre, sans image. Créez le composant card avec php artisan make:component card --view, et partez de cette structure :
<article {{ $attributes->class([]) }}>
@isset($image)
{{ $image }}
@endisset
<div>
{{ $slot }}
</div>
</article>
L'image arrive par un slot nommé, image, comme <x-slot:note> au chapitre Views. @isset ne l'affiche que si la page en donne une. Dans games/index.blade.php, chaque <article> devient :
<x-card>
<x-slot:image>
<img src="{{ asset($game->coverPath()) }}" alt="" width="400" height="300">
</x-slot:image>
<p>{{ $game->category }}</p>
<h2><a href="{{ route('games.show', $game->id) }}">{{ $game->title }}</a></h2>
<p>{{ $game->playersLabel() }} · {{ $game->duration }} min</p>
</x-card>
Dans styleguide.blade.php, remplacez de même l'<article> de la section « Carte » par <x-card>, sans slot image. Ajoutez ensuite les classes de la banque : celles du cadre dans le composant, celles de la grille sur la liste <ul>, et celles du contenu dans les deux vues.


Banque de classes, trois intrus compris :
| Classe | Effet |
|---|---|
object-cover |
recadre l'image pour remplir sa boîte, sans la déformer |
sm:grid-cols-2 |
deux colonnes à partir de 640 pixels |
tracking-wide |
lettres un peu plus espacées |
h-full |
toute la hauteur du parent |
whitespace-nowrap |
interdit le passage à la ligne |
rounded-xl |
coins arrondis de 12 pixels |
xl:grid-cols-4 |
quatre colonnes à partir de 1 280 pixels |
text-zinc-600 |
texte gris moyen |
p-5 |
marge intérieure de 20 pixels |
uppercase |
tout en majuscules |
hover:shadow-md |
ombre plus marquée au survol |
aspect-4/3 |
proportions de 4 de large pour 3 de haut |
max-w-sm |
largeur maximale de 24 rem |
grid |
range les enfants dans une grille |
tabular-nums |
chiffres de même largeur |
border border-zinc-200 |
bordure de 1 pixel, gris clair |
transition-shadow |
l'ombre change progressivement |
text-brand-700 |
texte bleu canard foncé |
lg:grid-cols-3 |
trois colonnes à partir de 1 024 pixels |
overflow-hidden |
coupe ce qui dépasse du cadre, comme les coins de l'image |
hover:underline hover:underline-offset-4 |
souligne au survol, 4 pixels sous le texte |
gap-6 |
24 pixels entre les lignes et entre les colonnes |
shadow-xs |
ombre très légère |
divide-y |
trait horizontal entre les enfants |
w-full |
toute la largeur du parent |
rounded-sm |
coins arrondis de 4 pixels, ceux du contour de focus compris |
bg-white |
fond blanc |
text-xs, text-sm, text-lg |
texte très petit, petit, grand |
font-semibold |
lettres épaisses |
mt-1, mt-2, mt-4 |
marge au-dessus de 4, 8 ou 16 pixels |
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-600 |
le contour de focus des boutons principaux, vu en 14.2 |
Critères
- Le catalogue affiche quatre cartes par ligne à 1 280 pixels, trois à 1 024, deux à 640 et une seule à 375.
- Dans une même ligne, les cartes ont toutes la même hauteur, même quand un titre passe sur deux lignes.
- La couverture remplit le haut de la carte sans être déformée, et ses coins suivent l'arrondi de la carte.
- Au survol d'une carte, son ombre se renforce progressivement. Au survol du titre, il se souligne.
- Au clavier, le titre de chaque jeu reçoit un contour bleu canard.
- La catégorie s'écrit en petites majuscules bleu canard, au-dessus du titre.
- Sur
/composants, la carte a le même cadre, sur 24 rem de large au plus. - Les classes du cadre n'apparaissent que dans
components/card.blade.php.
14.4 Le tableau des prêts et le badge de statut (25 minutes)
Chaque prêt est en cours, en retard ou rendu. Le modèle Loan calcule ce statut à partir des dates :
| Méthode | Ce qu'elle renvoie |
|---|---|
$loan->status() |
'ongoing', 'overdue' ou 'returned' |
$loan->statusLabel() |
'En cours', 'En retard' ou 'Rendu' |
Le badge. Créez le composant badge, avec une prop status. Écrivez ses classes avec @class, sur le modèle du badge du chapitre 13 : une chaîne toujours écrite, puis une chaîne par statut. Le texte du badge arrive par le slot :
<x-badge :status="$loan->status()">{{ $loan->statusLabel() }}</x-badge>
Au chapitre 13, le badge choisissait lui-même son texte avec match. Ici, le modèle fournit déjà ce texte avec statusLabel(). Dans styleguide.blade.php, remplacez les trois <span> par <x-badge status="ongoing">En cours</x-badge> et ses deux voisins.
Le tableau. Un tableau de six colonnes ne tient pas sur un téléphone de 375 pixels. Dans loans/index.blade.php, entourez <table> d'un <div> : c'est ce <div> qui défilera de côté, et non toute la page. Donnez ensuite leurs classes au <div>, à <table>, à <thead>, à <tbody>, aux lignes et aux cellules. Remplacez enfin {{ $loan->statusLabel() }} par le badge.


Banque de classes, trois intrus compris :
| Classe | Effet |
|---|---|
whitespace-nowrap |
interdit le passage à la ligne |
bg-red-50 text-red-700 ring-red-600/20 |
fond rouge très pâle, texte rouge, contour rouge à 20 % d'opacité |
min-w-full |
au moins toute la largeur du parent |
px-4 py-3 |
marge intérieure de 16 pixels sur les côtés, 12 en haut et en bas |
inline-flex items-center |
aligne le contenu au milieu, sans passer à la ligne avant ni après |
tabular-nums |
chiffres de même largeur, alignés d'une ligne à l'autre |
aspect-4/3 |
proportions de 4 de large pour 3 de haut |
overflow-x-auto |
ajoute une barre de défilement horizontale quand le contenu est trop large |
ring-1 ring-inset |
contour de 1 pixel, dessiné à l'intérieur |
hover:bg-zinc-50 |
fond gris très clair au survol |
divide-y divide-zinc-200 |
trait gris clair entre les enfants |
text-left |
texte aligné à gauche |
bg-zinc-100 text-zinc-600 ring-zinc-500/20 |
fond gris pâle, texte gris, contour gris à 20 % d'opacité |
rounded-full |
coins entièrement arrondis, en forme de pilule |
font-medium |
lettres moyennement épaisses |
object-cover |
recadre l'image pour remplir sa boîte |
bg-zinc-50 |
fond gris très clair |
px-2.5 py-0.5 |
marge intérieure de 10 pixels sur les côtés, 2 en haut et en bas |
text-zinc-500, text-zinc-400 |
texte gris, gris plus clair |
bg-sky-50 text-sky-700 ring-sky-600/20 |
fond bleu ciel très pâle, texte bleu, contour bleu à 20 % d'opacité |
xl:grid-cols-4 |
quatre colonnes à partir de 1 280 pixels |
text-xs, text-sm |
texte très petit, petit |
font-semibold |
lettres épaisses |
mt-4 flex flex-wrap gap-3 |
la rangée de boutons du guide de style, vue en 14.2 |
rounded-xl border border-zinc-200 bg-white shadow-xs |
le cadre de la carte, vu en 14.3 |
text-brand-700, avec les classes du lien de la carte |
le lien du titre, vu en 14.3, en bleu canard |
Critères
- À 375 pixels, la page ne défile pas de côté. Seul le tableau défile, à l'intérieur de son cadre arrondi.
- Le nom et l'adresse du membre restent sur deux lignes, et aucune autre cellule ne passe à la ligne.
- Les en-têtes sont en gras sur un fond gris très clair, et un trait fin sépare les lignes.
- Au survol, toute la ligne prend un fond gris très clair.
- Dans les colonnes de dates, les chiffres sont alignés d'une ligne à l'autre.
- Le nom du jeu est un lien bleu canard, souligné au survol, avec un contour au clavier.
- Un prêt en cours a un badge bleu, un retard un badge rouge, un jeu rendu un badge gris, ici comme sur
/composants. -
loans/index.blade.phpne contient aucune condition sur le statut : seulbadge.blade.phpchoisit les couleurs.
14.5 Le menu responsive (30 minutes)
Sur un téléphone, le logo et les trois liens du menu ne tiennent pas sur une ligne. Le menu se cache alors derrière un bouton, et s'affiche en ligne à partir de 768 pixels. Le lien de la page ouverte se distingue des deux autres.
Le lien de menu. Créez le composant nav-link, avec deux props : href, l'adresse, et active, qui vaut false par défaut. Partez de cette structure, et ajoutez ses classes avec @class : une chaîne toujours écrite, une pour le lien actif, une pour les autres. ! $active vaut true quand le lien n'est pas actif.
@props(['href', 'active' => false])
<a
href="{{ $href }}"
@if ($active) aria-current="page" @endif
>
{{ $slot }}
</a>
L'attribut aria-current="page" indique aux lecteurs d'écran que ce lien mène à la page ouverte. Dans le layout, les trois liens deviennent :
<li><x-nav-link :href="route('games.index')" :active="request()->routeIs('games.index', 'games.show')">Catalogue</x-nav-link></li>
<li><x-nav-link :href="route('loans.index')" :active="request()->routeIs('loans.*')">Prêts</x-nav-link></li>
<li><x-nav-link :href="route('games.create')" :active="request()->routeIs('games.create')">Ajouter un jeu</x-nav-link></li>
request()->routeIs() renvoie true quand le nom de la route de la page ouverte figure dans sa liste. 'loans.*' désigne toute route dont le nom commence par loans.. Le lien « Catalogue » reste actif sur la fiche d'un jeu, car elle appartient au catalogue.
Le bouton. Placez ce bouton entre le logo et <nav>, et donnez à <nav> l'attribut id="main-menu" :
<button type="button" id="menu-button" aria-controls="main-menu" aria-expanded="false">
<span class="sr-only">Menu</span>
<svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
- Les deux
<svg>dessinent trois traits, l'icône appelée burger, et une croix.stroke="currentColor"leur donne la couleur du texte du bouton. sr-onlycache le mot « Menu » à l'écran, mais les lecteurs d'écran le lisent : le bouton a un nom.group-aria-expanded:hiddencache les trois traits quand le parent marqué de la classegroupportearia-expanded="true".group-aria-expanded:blockaffiche alors la croix. La classegroup, dans la banque, va sur le bouton.
L'icône suit donc l'attribut aria-expanded, sans autre ligne de JavaScript.
Le script. Le code d'une page Laravel va dans resources/js/app.js, la seconde source de @vite. Le layout ne contient aucune balise <script> : Vite sert app.js pendant npm run dev, et l'inclut dans public/build/ au build. Écrivez-y le script du chapitre 13, section 6, avec deux changements :
- le script lit l'état du menu dans
aria-expanded, avecgetAttribute(), puis écrit l'état contraire, - il ne s'exécute que si
querySelector()a trouvé le bouton et le menu, carquerySelector()renvoienullpour un élément absent.


Banque de classes, trois intrus compris :
| Classe | Effet |
|---|---|
md:hidden |
cache l'élément à partir de 768 pixels |
group |
marque le parent que lisent les préfixes group-…: de ses enfants |
border-t border-zinc-200 |
trait gris clair au-dessus |
transition-colors |
les couleurs changent progressivement |
md:flex-row |
enfants en ligne à partir de 768 pixels |
bg-brand-50 text-brand-700 |
fond bleu canard très clair, texte bleu canard foncé |
tabular-nums |
chiffres de même largeur |
hidden |
cache l'élément |
md:w-auto |
largeur du contenu à partir de 768 pixels |
p-2 |
marge intérieure de 8 pixels |
md:block |
affiche l'élément à partir de 768 pixels |
flex flex-col |
enfants en colonne |
hover:bg-zinc-100 hover:text-zinc-900 |
fond gris pâle et texte presque noir au survol |
md:border-0 |
plus de bordure à partir de 768 pixels |
block |
le lien occupe toute la largeur de sa ligne, et toute sa surface est cliquable |
ring-inset |
contour dessiné à l'intérieur |
gap-1 |
4 pixels entre les enfants |
md:items-center |
enfants centrés en hauteur à partir de 768 pixels |
w-full |
toute la largeur du parent |
px-3 py-2 |
marge intérieure de 12 pixels sur les côtés, 8 en haut et en bas |
py-2, md:py-0 |
marge intérieure de 8 pixels en haut et en bas, aucune à partir de 768 pixels |
aspect-4/3 |
proportions de 4 de large pour 3 de haut |
rounded-md |
coins arrondis de 6 pixels |
text-zinc-600 |
texte gris moyen |
text-sm font-medium |
texte petit, lettres moyennement épaisses |
focus-visible:outline-2 focus-visible:outline-brand-600 |
contour de focus bleu canard, collé à l'élément |
Critères
- À 375 pixels, l'en-tête n'affiche que le logo et l'icône à trois traits.
- Un clic sur l'icône affiche les trois liens l'un sous l'autre, sous un trait, et l'icône devient une croix. Un second clic referme le menu.
- Dans l'onglet Éléments, le bouton passe de
aria-expanded="false"àaria-expanded="true"quand le menu s'ouvre. - À partir de 768 pixels, le bouton disparaît, et les trois liens s'alignent à droite du logo, menu fermé ou non.
- Le lien de la page ouverte a un fond bleu canard très clair et porte
aria-current="page". Sur la fiche d'un jeu, c'est « Catalogue ». - Au clavier, le bouton et chaque lien reçoivent un contour bleu canard.
- Le layout ne contient aucune balise
<script>.
4. D'après la capture : la fiche, le formulaire, le mode sombre
14.6 La fiche d'un jeu (20 minutes)
Mettez en forme games/show.blade.php. L'historique des prêts réutilise <x-badge>.

Critères
- Le lien « ← Retour au catalogue » est petit, bleu canard, souligné au survol, avec un contour au clavier.
- À partir de 768 pixels, la couverture et le texte sont côte à côte, en deux colonnes égales, alignés en haut. En dessous, la couverture est au-dessus du texte.
- La couverture a des coins arrondis et une ombre légère, sans être déformée.
- La catégorie et le titre reprennent le style du catalogue. Le titre a la taille des autres titres de page.
- La description a des lignes plus espacées que le texte courant.
- Joueurs, durée et nombre de prêts sont sur trois colonnes, sous un trait. Chaque libellé gris est au-dessus de sa valeur en gras.
- L'historique est une liste encadrée, avec un trait entre deux prêts et le badge à droite. Si la place manque, le badge passe sous la date.
14.7 Le formulaire d'ajout (25 minutes)
Mettez en forme games/create.blade.php. Le message sous le champ « Durée » est écrit en dur, car le formulaire ne vérifie encore rien : il sert à donner son style à un champ en erreur. Les attributs du champ sont déjà en place. aria-invalid="true" annonce aux lecteurs d'écran que la valeur est refusée, et aria-describedby="duration-error" relie le champ à son message.

Critères
- Le titre et le formulaire sont centrés, sur 42 rem de large au plus. Le formulaire a le cadre d'une carte, et ses blocs sont espacés régulièrement.
- Chaque libellé est au-dessus de son champ. Un clic sur le libellé place le curseur dans le champ.
- Les champs occupent toute la largeur du cadre, et « Minimum » et « Maximum » se partagent une ligne.
- Au focus, la bordure du champ devient bleu canard, et un halo bleu canard pâle l'entoure, au clic comme au clavier.
- Le champ « Durée » a une bordure rouge et son message est rouge. Au focus, son halo est rouge.
- L'aide « Facultatif. Deux ou trois phrases suffisent. » est grise et plus petite que le libellé.
- À 375 pixels, le bouton « Ajouter le jeu » occupe toute la largeur, au-dessus du lien « Annuler ». À partir de 640 pixels, les deux sont alignés à droite, sur une ligne.
- Le lien « Annuler » reçoit un contour bleu canard au clavier.
Le code qui sent mauvais
En binôme, cinq minutes. Dans le corrigé de 14.7, les six champs répètent presque la même liste de quatorze classes. La ludothèque veut maintenant des champs aux coins moins arrondis, rounded-md au lieu de rounded-lg. Combien de lignes modifiez-vous, et que faudrait-il créer pour n'en modifier qu'une ?
14.8 Bonus : le mode sombre (30 minutes)
Le préfixe dark: du chapitre 13 applique une classe quand le système est réglé en mode sombre. Donnez une version sombre à tout le site : le layout, les quatre composants et les cinq vues.
Pour tester sans changer le réglage de votre système, ouvrez les outils de développement de Chrome ou d'Edge avec F12. Appuyez ensuite sur Ctrl+Maj+P, tapez dark, et choisissez « Emulate CSS prefers-color-scheme: dark ».

Critères
- Sur les cinq pages, le fond est presque noir, et les surfaces sont un peu plus claires : en-tête, cartes, tableau, formulaire.
- Tout texte reste lisible. Les gris passent à une nuance plus claire, et le bleu canard à
brand-300. - Les bordures et les traits de séparation restent visibles, sans briller.
- Les trois badges, le bouton secondaire et le lien actif du menu ont leur version sombre. Les boutons principal et danger ne changent pas.
- Les champs du formulaire ont un fond sombre, et le texte tapé est clair.
- De retour en mode clair, rien n'a changé.
À retenir
@vitedans le<head>du layout charge le CSS et le JavaScript de toutes les pages, etcomposer run devdoit tourner pour que chaque nouvelle classe ait sa règle.- Un conteneur
mx-auto max-w-6xlrépété avec les mêmes marges aligne l'en-tête, le contenu et le pied de page. - Un composant Blade écrit une liste de classes une fois, choisit ses variantes avec
@classou$attributes->class(), et laisse la page ajouter une marge. - Une classe sans préfixe vise le téléphone, et
sm:,md:,lg:ouxl:la remplacent à partir d'une largeur. - Le JavaScript d'une page va dans
resources/js/app.js, et il tient l'attributaria-expandedà jour pour les lecteurs d'écran.