Changer le thème

Astro + Tailwind : configurer les styles sans conflit avec les composants islands

Easton editorial illustration: modular system blueprint

Ouvrez les outils de développement du navigateur : des règles CSS barrées en rouge partout. Hier tout allait bien, puis vous avez ajouté une directive client:load et tout s’est effondré — les espacements ont disparu, la grille Grid est cassée, et même le sélecteur :nth-child le plus basique ne cible plus le bon élément.

En inspectant le DOM, vous découvrez deux balises inconnues : astro-island et astro-slot. Elles n’apparaissent nulle part dans votre code !

Si vous utilisez l’architecture islands d’Astro, vous rencontrerez probablement ce genre de situation. Ce n’est pas un bug, c’est le fonctionnement normal d’Astro. Le problème, c’est que beaucoup de tutoriels expliquent comment intégrer Tailwind, mais pas les pièges CSS propres aux islands. Cet article rassemble les écueils que j’ai rencontrés pour vous aider à éviter ces mines de styles.

À la fin de la lecture, vous comprendrez comment les islands modifient la structure DOM, pourquoi certains sélecteurs CSS cessent soudain de fonctionner, comment configurer correctement Tailwind v4 dans Astro, et comment résoudre quatre scénarios courants de conflits de styles.

1. Comment l’architecture islands affecte le rendu des styles

Commençons par une chose : l’architecture islands d’Astro ne « casse » pas les styles en soi — elle change simplement la structure du DOM. Le problème vient du fait qu’on ignore ce changement et qu’on continue d’écrire du CSS de manière traditionnelle.

Comportement par défaut : HTML statique, zéro JS

Le principe d’Astro est simple — rendu HTML statique par défaut, avec suppression automatique de tout JavaScript côté client. Un composant écrit ainsi :

---
import Counter from './Counter.svelte'
---

<Counter />

Ne produit que du HTML + CSS, sans JavaScript. C’est excellent pour les performances : chargement rapide, SEO favorable. Mais pour le rendre interactif, il faut ajouter une directive d’hydratation client :

<Counter client:load />

Et là, la structure DOM change.

L’apparition soudaine de astro-island et astro-slot

Avec client:load, Astro enveloppe votre composant dans une balise astro-island. S’il contient un slot, une balise astro-slot s’ajoute aussi.

Prenons un composant carte :

---
import Card from './Card.svelte'
---

<Card client:load>
  <div>Contenu de la carte</div>
</Card>

Vous vous attendez à :

<div class="card">
  <div>Contenu de la carte</div>
</div>

En réalité :

<astro-island>
  <div class="card">
    <astro-slot>
      <div>Contenu de la carte</div>
    </astro-slot>
  </div>
</astro-island>

Vous voyez le problème ? Un astro-slot s’intercale, et votre sélecteur .card > div ne fonctionne plus : le div n’est plus un enfant direct de .card.

Pire encore, astro-island et astro-slot utilisent tous deux display: contents. Cette propriété CSS fait « disparaître » l’élément du layout — il reste dans le DOM, mais ne participe plus au modèle de boîte. Impossible de lui définir largeur, hauteur, marges ou positionnement ; grid-column en Grid ne lui s’applique pas non plus.

Les composants statiques n’ont pas ce problème

Sans directive d’hydratation :

<Card>
  <div>Contenu de la carte</div>
</Card>

Astro ne crée ni astro-island ni astro-slot. Le DOM correspond à vos attentes :

<div class="card">
  <div>Contenu de la carte</div>
</div>

Le même composant peut donc avoir ces balises supplémentaires ou non. Comment écrire du CSS qui fonctionne dans les deux cas ? C’est le cœur du problème que nous allons résoudre.

2. Intégration correcte de Tailwind CSS : v4 vs v3

Pour Tailwind, beaucoup pensent d’abord à npx astro add tailwind. C’est effectivement le plus simple, mais avec Tailwind v4, les choses changent un peu.

La nouvelle intégration v4

Tailwind v4 propose un plugin Vite officiel : @tailwindcss/vite. Il est plus concis que l’ancien @astrojs/tailwind et correspond à la recommandation officielle de Tailwind.

Étapes concrètes :

1. Installer les dépendances

npm install tailwindcss @tailwindcss/vite

2. Configurer astro.config.mjs

import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});

3. Créer un fichier CSS global

Dans src/styles/global.css :

@import "tailwindcss";

4. Importer dans le Layout

---
import '../styles/global.css';
---

<html>
  <slot />
</html>

C’est tout. Bien plus simple que le trio v3 @tailwind base; @tailwind components; @tailwind utilities;.

Et si vous êtes encore en v3 ?

Deux approches :

Option A : intégration @astrojs/tailwind

npx astro add tailwind

Cela génère automatiquement tailwind.config.cjs et ajoute l’intégration dans astro.config.mjs. Attention au piège : Tailwind base est injecté sur chaque page — vous ne contrôlez pas quelles pages utilisent Tailwind.

Option B : configuration PostCSS manuelle

Créez postcss.config.cjs :

module.exports = {
  plugins: {
    tailwindcss: {},
  },
};

Puis créez src/styles/tailwind.css et importez-le dans le Layout souhaité. Contrôle total.

Ne pas se tromper sur content

Que ce soit v3 ou v4, le point crucial est la config content. Beaucoup de styles « qui ne marchent pas » viennent d’une omission des fichiers .astro :

// tailwind.config.cjs
module.exports = {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  // ...
};

Notez bien .astro. Sans lui, les classes Tailwind dans vos composants Astro ne seront générées nulle part à la compilation.

3. Quatre scénarios de conflits de styles et leurs solutions

C’est le cœur de l’article. J’ai regroupé tous les problèmes de styles que j’ai rencontrés. Chaque scénario inclut le code problématique, l’analyse et la correction.

Scénario 1 : sélecteur enfant direct invalide

Code problématique :

/* Ce CSS échoue quand le composant a une directive d'hydratation */
.Card > div {
  padding: 1rem;
  background: #f0f0f0;
}

Pourquoi ça échoue :

La structure DOM a changé. Un astro-slot s’intercale :

<div class="Card">
  <astro-slot> <!-- inséré ici -->
    <div>Contenu</div>
  </astro-slot>
</div>

.Card > div ne cible plus ce div, qui n’est plus enfant direct de .Card.

Solution A (recommandée) : sélecteur descendant

.Card div {
  padding: 1rem;
  background: #f0f0f0;
}

Simple et direct. Attention si vous avez beaucoup de niveaux d’imbrication — vous pourriez cibler des éléments indésirables.

Solution B : inclure astro-slot dans la chaîne de sélecteurs

CSS global :

.Card > astro-slot > div {
  padding: 1rem;
  background: #f0f0f0;
}

CSS Scoped :

<style>
.Card :global(> astro-slot > div) {
  padding: 1rem;
  background: #f0f0f0;
}
</style>

Plus précis, mais plus verbeux. À choisir selon la complexité du projet.

Scénario 2 : sélecteur Lobotomized owl inefficace

Code problématique :

/* Technique classique d'espacement */
.List > * + * {
  margin-top: 1rem;
}

Ce sélecteur signifie : pour chaque enfant du conteneur parent, s’il a un frère précédent, ajouter une marge supérieure. Très utile, mais inefficace dans les islands.

Pourquoi ça échoue :

astro-island et astro-slot utilisent display: contents — ils « disparaissent » du layout. Mais * + * les sélectionne quand même, et les styles sur display: contents sont ignorés.

Solution :

.List > * + *,
.List > * + :where(astro-island, astro-slot) > *:first-child {
  margin-top: 1rem;
}

Cette syntaxe « traverse » astro-island et astro-slot pour appliquer la marge au premier enfant interne. Complexe à lire, mais efficace.

Scénario 3 : échec du positionnement CSS Grid

Code problématique :

---
import Item from './Item.svelte'
---

<div class="Grid">
  <Item client:load />
  <Item client:load />
  <Item client:load />
</div>

<style>
.Grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1em;
}

/* Faire occuper toute la ligne au premier élément */
.Grid > *:first-child {
  grid-column: 1 / -1;
}
</style>

Résultat : le premier élément n’occupe pas toute la ligne.

Pourquoi ça échoue :

grid-column ne s’applique pas à astro-island à cause de display: contents.

Solution A : contourner les islands

.Grid > *,
.Grid > :where(astro-island, astro-slot) > *:first-child {
  grid-column: 1 / -1;
}

Solution B : élément wrapper

<div class="Grid">
  <div><Item client:load /></div>
  <div><Item client:load /></div>
  <div><Item client:load /></div>
</div>

grid-column s’applique alors au div, sans être affecté par les islands. Je préfère cette approche : code clair et lisible.

Scénario 4 : décalage du sélecteur nth-child

Code problématique :

/* Cibler le premier composant */
.Grid > *:nth-child(1) {
  background: red;
}

Résultat : le premier composant ne devient pas rouge, mais d’autres éléments de la page sont affectés.

Pourquoi ça échoue :

Astro insère des balises style et script à côté des composants. Elles comptent aussi comme enfants ; nth-child les inclut.

Solution A : utiliser nth-of-type

.Grid > astro-island:nth-of-type(1) > .Item {
  background: red;
}

Solution B : élément wrapper

<div class="Grid">
  <div><Item client:load /></div>
  <div><Item client:load /></div>
</div>

<style>
.Grid > *:nth-child(1) .Item {
  background: red;
}
</style>

Franchement, dans ce cas, je recommande fortement le wrapper. nth-of-type devient vite illisible et coûteux à maintenir.

4. Matrice de choix des styles : quand utiliser Tailwind / Scoped / Global

Astro offre beaucoup d’options de style — parfois trop. Voici une stratégie simple :

Tailwind : développement rapide, système de design unifié

Convient à :

  • Layout (structure globale de la page)
  • Prototypage rapide
  • Langage de design cohérent
  • Quand vous ne voulez pas écrire de CSS personnalisé

Moins adapté à :

  • Styles de composants très personnalisés
  • Sélecteurs complexes (comme les problèmes islands ci-dessus)

Exemple :

---
import Header from './Header.astro'
---

<div class="max-w-7xl mx-auto px-4 py-8">
  <Header />
  <main class="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
    <slot />
  </main>
</div>

Clair et immédiatement lisible.

Scoped CSS : styles internes au composant, sans pollution

Convient à :

  • Styles internes aux composants
  • Sélecteurs spécifiques (:hover, :focus)
  • Isoler les styles sans impacter les autres composants

Moins adapté à :

  • Styles de base globaux
  • Styles partagés entre composants

Exemple :

<div class="card">
  <h2>Titre</h2>
  <p>Contenu</p>
</div>

<style>
.card {
  padding: 1.5rem;
  border-radius: 8px;
  background: white;
}

.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>

Ces styles ne s’appliquent qu’à ce composant, sans affecter les autres .card.

Global CSS : styles de base globaux

Convient à :

  • CSS reset / normalize
  • Variables de thème (custom properties)
  • Styles base Tailwind
  • Polices et couleurs globales

Moins adapté à :

  • Styles internes aux composants (risque de pollution)

Exemple :

/* src/styles/global.css */
@import "tailwindcss";

:root {
  --color-primary: #2563eb;
  --font-sans: 'Inter', sans-serif;
}

body {
  font-family: var(--font-sans);
  color: #1a1a1a;
}

Une seule importation dans le Layout suffit.

CSS Modules : allié des composants complexes

Astro supporte aussi CSS Modules — suffixe .module.css :

---
import styles from './Card.module.css'
---

<div class={styles.card}>
  <h2 class={styles.title}>Titre</h2>
</div>

Convient à :

  • Composants complexes avec de nombreuses classes
  • Mapping de classes pour éviter les conflits
  • Usage mixte avec Tailwind

Ma combinaison recommandée :

  1. Layout : Global CSS + Tailwind (layout et styles globaux)
  2. Intérieur des composants : Scoped CSS en priorité (bon isolement)
  3. Cas particuliers : CSS Modules (composants complexes) ou Tailwind (développement rapide)
  4. À éviter : mélanger trop d’approches — 2 à 3 suffisent

5. Bonnes pratiques et checklist anti-pièges

Enfin, une checklist des pièges que j’ai rencontrés :

1. Stratégie de priorité des sélecteurs

À éviter :

  • Dépendance excessive aux sélecteurs enfant direct (>)
  • nth-child là où il y a des islands

À privilégier :

  • Sélecteurs descendants (espace)
  • nth-of-type à la place de nth-child
  • Éléments wrapper pour isoler l’impact des islands

2. Flux de débogage des styles

En cas de problème de style, vérifiez dans cet ordre :

  1. Ouvrir les outils de développement, inspecter le DOM — y a-t-il astro-island ou astro-slot ?
  2. Vérifier le chemin du sélecteur — cible-t-il vraiment l’élément voulu ?
  3. Consulter les styles calculésdisplay: contents invalide-t-il certains styles ?
  4. Vérifier l’ordre d’import CSS — à spécificité égale, le dernier importé l’emporte

3. Configuration content Tailwind

Mauvaise écriture :

content: ['./src/**/*.{html,js,jsx}']  // .astro manquant

Bonne écriture :

content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}']

Sans .astro, aucune classe Tailwind dans vos composants Astro ne sera générée.

4. Conseils d’optimisation des performances

Éviter la sur-hydratation :

<!-- Déconseillé : client:load sur tous les composants -->
<Header client:load />
<Content client:load />
<Footer client:load />

<!-- Recommandé : hydratation uniquement où nécessaire -->
<Header client:load />
<Content />  <!-- contenu statique, pas de JS -->
<Footer />   <!-- contenu statique, pas de JS -->

Préférer client:visible à client:load :

Si le composant n’est pas au-dessus de la ligne de flottaison, ou que l’utilisateur ne le verra pas forcément, utilisez client:visible. Le JS ne se charge que lorsque le composant entre dans le viewport — moins de bande passante, chargement plus rapide.

<ImageCarousel client:visible />

5. Ordre d’import CSS

Dans Astro, l’ordre d’import CSS influence la priorité. À spécificité égale, le dernier importé gagne.

Approche recommandée :

---
// Layout.astro
import '../styles/global.css';  // styles globaux en premier
import '../styles/tailwind.css'; // Tailwind ensuite
---

<html>
  <slot />
</html>

Les utilitaires Tailwind peuvent ainsi surcharger les styles globaux.

6. L’élément wrapper, votre allié

Beaucoup de problèmes de style liés aux islands se résolvent avec un wrapper :

<div class="grid gap-4">
  <div><Item client:load /></div>
  <div><Item client:load /></div>
</div>

Un niveau d’imbrication en plus, mais code clair, sélecteurs simples, maintenance facile. Ne sacrifiez pas la lisibilité pour une « propreté » du code qui vous piégera.

Conclusion

En résumé : comprendre comment l’architecture islands d’Astro modifie le DOM, puis adapter votre écriture CSS.

Points clés :

  1. Les directives d’hydratation créent astro-island et astro-slot — ils utilisent display: contents et perturbent les sélecteurs
  2. Tailwind v4 via le plugin @tailwindcss/vite — intégration plus simple qu’en v3
  3. Évitez les sélecteurs enfant direct et nth-child — préférez descendants, nth-of-type ou wrapper
  4. Combinaison de styles — Global + Tailwind pour le Layout, Scoped pour les composants, Modules si besoin

Si vous rencontrez un problème de style, ouvrez d’abord les outils de développement et inspectez le DOM. Souvent, le CSS n’est pas « faux » — c’est la structure DOM qui a changé sans que vous le remarquiez.

Vérifiez la config Tailwind de votre projet, passez au plugin Vite v4 si ce n’est pas fait, puis appliquez les méthodes de cet article pour traquer les conflits liés aux islands. Une fois corrigé, votre code sera nettement plus clair.

FAQ

Pourquoi les styles se cassent-ils après l'ajout de client:load ?
Les directives d'hydratation (comme client:load) créent les balises astro-island et astro-slot, qui utilisent display: contents. Cela modifie la structure DOM et invalide les sélecteurs enfant direct (>), nth-child, etc. Utilisez des sélecteurs descendants ou un élément wrapper pour contourner le problème.
Comment configurer Tailwind v4 dans Astro ?
Tailwind v4 recommande le plugin @tailwindcss/vite. Étapes :

1. Installation : npm install tailwindcss @tailwindcss/vite
2. Ajoutez-le dans vite.plugins de astro.config.mjs
3. Créez un CSS global avec @import 'tailwindcss'
4. Importez-le dans le Layout

Beaucoup plus simple qu'en v3, sans @tailwind base/components/utilities.
Que sont astro-island et astro-slot ?
Ce sont des balises internes de l'architecture islands d'Astro. Lorsqu'un composant reçoit une directive d'hydratation, Astro crée automatiquement ces balises pour gérer l'hydratation. Elles utilisent display: contents : elles « disparaissent » du layout mais modifient la correspondance des sélecteurs CSS.
Quels sélecteurs CSS posent le plus de problèmes ?
Les quatre catégories les plus fragiles :

1. Sélecteur enfant direct (>) — astro-slot s'intercale au milieu
2. Lobotomized owl (* + *) — les styles sur display: contents sont ignorés
3. Positionnement Grid (grid-column) — inefficace sur display: contents
4. nth-child — les balises style/script comptent aussi comme enfants

Solutions : sélecteurs descendants, nth-of-type, ou élément wrapper.
Pourquoi les classes Tailwind ne s'appliquent-elles pas ?
La cause la plus fréquente : la config content omet les fichiers .astro. Configuration correcte : content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}']. Sans l'extension .astro, les classes Tailwind dans les composants Astro ne seront ni scannées ni générées.
Quand utiliser Scoped CSS et quand utiliser Global ?
Règle simple :

- Couche Layout : Global CSS + Tailwind (layout et styles globaux)
- Intérieur des composants : Scoped CSS (bon isolement, sans impact sur les autres)
- Composants complexes : CSS Modules (nombreuses classes, mapping nécessaire)
- Développement rapide : Tailwind (langage de design unifié)

Évitez de mélanger trop d'approches ; 2 à 3 suffisent.

11 min de lecture · Publié le: 31 mars 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog