Skip to content

Astro Tailwind v4 + React

Cette variante garde la même architecture que test-bootstrap, mais remplace Bootstrap/jQuery par Tailwind v4 et React.

Astro est responsable des pages, du layout et du build statique. React sert à écrire des composants .tsx. Tailwind sert au style. Le JavaScript global reste possible pour des comportements non React, mais il ne doit pas dupliquer une interaction déjà gérée par React.

Reprendre la même séparation que le projet Bootstrap.

test-tailwind-react/
astro.config.mjs
package.json
public/
favicon.ico
favicon.svg
navbar.js
src/
assets/
hero.svg
logo.svg
components/
Hero.tsx
Navbar.tsx
layouts/
Layout.astro
pages/
index.astro
styles/
custom.css
env.d.ts

Les rôles restent stables :

  1. src/pages/ déclare les routes Astro.
  2. src/layouts/Layout.astro contient le document HTML commun.
  3. src/components/*.tsx contient les composants React.
  4. src/styles/custom.css importe Tailwind et les styles projet.
  5. public/navbar.js peut contenir un JavaScript global livré tel quel dans dist/.
  6. src/assets/ contient les images importées dans Astro ou React.
  7. public/ contient les favicons et fichiers servis sans transformation.

Créer Astro avec React, puis installer Tailwind v4.

Terminal window
npm create astro@latest
npx astro add react
npm install tailwindcss @tailwindcss/vite

Dépendances minimales :

package.json
{
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",
"astro": "^6.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.0"
}
}

Ajouter React et Tailwind au build Astro.

astro.config.mjs
// @ts-check
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
output: "static",
integrations: [react()],
build: {
assetsPrefix: ".",
},
vite: {
plugins: [tailwindcss()],
build: {
cssCodeSplit: true,
assetsInlineLimit: 0,
},
},
});

Points importants :

  1. react() permet d’utiliser des fichiers .tsx.
  2. tailwindcss() active Tailwind v4 dans Vite.
  3. output: "static" garde une sortie HTML statique.
  4. Sans directive client:*, les composants React sont rendus en HTML au build.

Dans Tailwind v4, le CSS global importe Tailwind directement.

src/styles/custom.css
@import "tailwindcss";
@theme {
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
}
.test-text {
color: #b45309;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}

Conventions :

  1. garder le nom custom.css pour rester proche de test-bootstrap ;
  2. mettre @import "tailwindcss"; en première ligne ;
  3. réserver @theme aux tokens projet ;
  4. garder les classes utilitaires Tailwind dans les composants .tsx ;
  5. utiliser des classes CSS nommées seulement quand elles représentent une convention projet.

Le layout importe custom.css, pose les balises globales et expose le slot.

src/layouts/Layout.astro
---
import "../styles/custom.css";
---
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="icon" href="./favicon.ico" />
<meta name="generator" content={Astro.generator} />
<title>Astro Tailwind React</title>
</head>
<body class="min-h-screen bg-slate-950 text-white antialiased">
<slot />
<script is:inline src="./navbar.js"></script>
</body>
</html>

Le script global est optionnel dans cette variante. S’il doit être livré tel quel, le placer dans public/.

Le garder si :

  1. un comportement doit rester hors React ;
  2. le back doit reprendre un fichier JavaScript classique ;
  3. l’interaction ne justifie pas une hydratation React.

Le retirer si :

  1. tous les composants sont statiques ;
  2. l’interaction est gérée par un composant React hydraté ;
  3. le build final doit contenir le moins de JavaScript possible.

La page Astro assemble les composants React.

src/pages/index.astro
---
import Hero from "../components/Hero";
import Navbar from "../components/Navbar";
import Layout from "../layouts/Layout.astro";
---
<Layout>
<Navbar />
<Hero />
</Layout>

Un composant .tsx peut remplacer un composant .astro quand le projet préfère React pour le templating.

src/components/Hero.tsx
import type { FC } from "react";
const Hero: FC = () => {
return (
<main className="bg-blue-700 text-white">
<section className="mx-auto max-w-5xl px-6 py-20">
<h1 className="text-5xl font-semibold tracking-normal">Bienvenue !</h1>
<p className="mt-4 max-w-2xl text-lg text-blue-100">
Ceci est un hero simple construit avec Astro, Tailwind v4 et React.
</p>
<p className="test-text mt-6">
Tailwind gère la mise en page, les couleurs et la typographie.
</p>
<button
className="mt-8 rounded-md bg-white px-4 py-2 text-sm font-medium text-blue-700"
type="button"
>
En savoir plus
</button>
</section>
</main>
);
};
export default Hero;

Ce composant est statique tant que la page l’utilise comme ceci :

<Hero />

Il devient hydraté si la page l’utilise comme ceci :

<Hero client:load />

Pour une sortie HTML statique, la navbar React ne doit pas dépendre de useState.

src/components/Navbar.tsx
import type { FC } from "react";
const Navbar: FC = () => {
return (
<nav className="bg-slate-950 text-white">
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
<span className="text-base font-semibold">Mon Site</span>
<button
id="navbar-toggle"
className="rounded-md border border-white/20 px-3 py-2 text-sm lg:hidden"
type="button"
aria-controls="navbarNav"
aria-expanded="false"
>
Menu
</button>
<div id="navbarNav" className="hidden items-center gap-6 lg:flex">
<button className="text-sm text-white" type="button">
Accueil
</button>
<button className="text-sm text-white/70" type="button">
À propos
</button>
<button className="text-sm text-white/70" type="button">
Contact
</button>
<button
id="notif-btn"
className="rounded-md border border-white/20 px-3 py-2 text-sm"
type="button"
>
Notifications
<span id="notif-count" className="ml-2 rounded bg-red-600 px-2 py-0.5">
3
</span>
</button>
</div>
</div>
</nav>
);
};
export default Navbar;

Dans ce modèle, l’ouverture mobile et le compteur peuvent rester dans public/navbar.js.

public/navbar.js
document.addEventListener("DOMContentLoaded", function () {
var count = 3;
var notifBtn = document.getElementById("notif-btn");
var notifCount = document.getElementById("notif-count");
var navbarToggle = document.getElementById("navbar-toggle");
var navbarNav = document.getElementById("navbarNav");
if (notifBtn && notifCount) {
notifBtn.addEventListener("click", function () {
count += 1;
notifCount.textContent = String(count);
});
}
if (navbarToggle && navbarNav) {
navbarToggle.addEventListener("click", function () {
var isOpen = navbarNav.classList.toggle("flex");
navbarNav.classList.toggle("hidden", !isOpen);
navbarToggle.setAttribute("aria-expanded", String(isOpen));
});
}
});

Cette version n’a pas besoin de jQuery.

Si l’interaction doit rester dans React, mettre l’état dans le composant et hydrater explicitement.

src/components/Navbar.tsx
import { useState, type FC } from "react";
const Navbar: FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [count, setCount] = useState(3);
return (
<nav className="bg-slate-950 text-white">
<button type="button" onClick={() => setIsOpen((value) => !value)}>
Menu
</button>
<div className={isOpen ? "block" : "hidden"}>
<button type="button" onClick={() => setCount((value) => value + 1)}>
Notifications <span>{count}</span>
</button>
</div>
</nav>
);
};
export default Navbar;

Et dans la page :

src/pages/index.astro
<Navbar client:load />

Choisir cette option seulement si le projet accepte du JavaScript React dans le navigateur.

Deux cas :

  1. src/assets/ pour les images importées par Astro ou React ;
  2. public/ pour les fichiers servis tels quels.

Dans un composant React, importer une image depuis src/assets/.

src/components/Hero.tsx
import heroUrl from "../assets/hero.svg";
export default function Hero() {
return <img src={heroUrl.src} alt="" />;
}

Pour un favicon ou un fichier qui doit garder son chemin, utiliser public/.

public/
favicon.ico
favicon.svg

Lancer :

Terminal window
npm run build

Sortie attendue :

dist/
index.html
favicon.ico
favicon.svg
_astro/
index@_@astro.[hash].css
client.[hash].js

Le fichier client.[hash].js peut exister sans être chargé par la page. Ce qui compte est le HTML final :

  1. si aucun composant n’a client:*, le HTML contient le rendu statique ;
  2. si un composant a client:load, le HTML charge le runtime nécessaire ;
  3. si navbar.js est utilisé, vérifier qu’il est bien copié dans dist/navbar.js et chargé depuis dist/index.html.

Choisir Tailwind v4 + React + .tsx si :

  1. le projet veut composer plus vite avec React ;
  2. les composants ont des props ou variantes plus lisibles en TSX ;
  3. le style doit être porté par des classes utilitaires ;
  4. le build doit rester statique par défaut ;
  5. l’hydratation React doit rester un choix explicite.