frontend: migrate to Tailwind 4

Tailwind 4 ships its own Vite plugin and no longer needs PostCSS plumbing.
Drop postcss/autoprefixer/postcss-import/postcss-nesting and the .postcssrc.json,
wire @tailwindcss/vite into vite.config.ts, replace the @tailwind directives
with @import "tailwindcss", and convert the JS tailwind.config.cjs theme
into a CSS @theme block in shared.css. An @source directive points back at
the SSR templates so the Jinja-rendered HTML in templates/ still gets scanned
for utility classes.

The third-party CSS (compound design tokens, compound-web, fontsource) moves
into a new vendor.css entrypoint, loaded ahead of shared.css from main.tsx,
storybook, and base.html. Tailwind v4's @import bundler silently drops the
nested `@import url(...) layer(cpd-base) screen` statements inside the
compound-design-tokens.css barrel, so we have to keep those imports out of
any file that contains Tailwind directives and let Vite's normal CSS pipeline
resolve them instead.
This commit is contained in:
Quentin Gliech
2026-05-25 13:19:11 +02:00
parent 08898b10da
commit 8e65bf198e
10 changed files with 281 additions and 492 deletions
-8
View File
@@ -1,8 +0,0 @@
{
"plugins": {
"postcss-import": {},
"tailwindcss/nesting": "postcss-nesting",
"tailwindcss": {},
"autoprefixer": {}
}
}
+1
View File
@@ -9,6 +9,7 @@ import { TooltipProvider } from "@vector-im/compound-web";
import { initialize, mswLoader } from "msw-storybook-addon";
import { useEffect, useLayoutEffect } from "react";
import { I18nextProvider } from "react-i18next";
import "../src/entrypoints/vendor.css";
import "../src/entrypoints/shared.css";
import i18n, { setupI18n } from "../src/i18n";
import { DummyRouter } from "../src/test-utils/router";
+2 -5
View File
@@ -49,6 +49,7 @@
"@graphql-typed-document-node/core": "^3.2.0",
"@storybook/addon-docs": "^10.4.1",
"@storybook/react-vite": "^10.4.1",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-query-devtools": "^5.100.14",
"@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.11",
@@ -61,7 +62,6 @@
"@types/swagger-ui-dist": "^3.30.6",
"@vitejs/plugin-react": "^6.0.2",
"@vitest/coverage-v8": "^4.1.7",
"autoprefixer": "^10.5.0",
"browserslist-to-esbuild": "^2.1.1",
"graphql": "^16.14.0",
"happy-dom": "^20.9.0",
@@ -69,12 +69,9 @@
"knip": "^6.14.2",
"msw": "^2.14.6",
"msw-storybook-addon": "^2.0.7",
"postcss": "^8.5.15",
"postcss-import": "^16.1.1",
"postcss-nesting": "^14.0.0",
"rimraf": "^6.1.3",
"storybook": "^10.4.1",
"tailwindcss": "^3.4.19",
"tailwindcss": "^4.3.0",
"tinyglobby": "^0.2.16",
"typescript": "^6.0.3",
"vite": "8.0.14",
+1
View File
@@ -15,6 +15,7 @@ import LoadingScreen from "../components/LoadingScreen";
import { queryClient } from "../graphql";
import i18n, { setupI18n } from "../i18n";
import { router } from "../router";
import "./vendor.css";
import "./shared.css";
setupI18n();
+30 -11
View File
@@ -5,15 +5,34 @@
* Please see LICENSE files in the repository root for full details.
*/
@import "@fontsource/inter/400.css";
@import "@fontsource/inter/500.css";
@import "@fontsource/inter/600.css";
@import "@fontsource/inter/700.css";
@import "@fontsource/inconsolata/400.css";
@import "@fontsource/inconsolata/700.css";
@import "@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css";
@import "@vector-im/compound-web/dist/style.css";
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
/* The SSR templates live outside the frontend root, so they aren't picked up by
* Tailwind's automatic content detection. */
@source "../../../templates/**/*.html";
@theme {
/* Replace the default palette and font weights with our own tokens. */
--color-*: initial;
--font-weight-*: initial;
--color-white: #ffffff;
--color-primary: var(--cpd-color-text-primary);
--color-secondary: var(--cpd-color-text-secondary);
--color-critical: var(--cpd-color-text-critical-primary);
--color-alert: #ff5b55;
--color-links: #0086e6;
--color-grey-25: #f4f6fa;
--color-grey-50: #e3e8f0;
--color-grey-100: #c1c6cd;
--color-grey-150: #8d97a5;
--color-grey-200: #737d8c;
--color-grey-250: #a9b2bc;
--color-grey-300: #8e99a4;
--color-grey-400: #6f7882;
--color-grey-450: #394049;
--font-weight-semibold: var(--cpd-font-weight-semibold);
--font-weight-medium: var(--cpd-font-weight-medium);
--font-weight-regular: var(--cpd-font-weight-regular);
}
+21
View File
@@ -0,0 +1,21 @@
/* Copyright 2024, 2025 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
/* Third-party styles. Kept out of shared.css because @tailwindcss/vite's
* @import bundler drops the nested `@import url(...) layer(cpd-base) screen`
* declarations inside compound-design-tokens.css. Loading them from a CSS file
* with no Tailwind directives lets Vite resolve them through its normal
* Lightning-CSS pipeline, which preserves the layer and media qualifiers. */
@import "@fontsource/inter/400.css";
@import "@fontsource/inter/500.css";
@import "@fontsource/inter/600.css";
@import "@fontsource/inter/700.css";
@import "@fontsource/inconsolata/400.css";
@import "@fontsource/inconsolata/700.css";
@import "@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css";
@import "@vector-im/compound-web/dist/style.css";
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
// @ts-check
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: "jit",
content: ["./src/**/*.tsx", "./index.html", "../templates/**/*.html"],
theme: {
colors: {
white: "#FFFFFF",
primary: "var(--cpd-color-text-primary)",
secondary: "var(--cpd-color-text-secondary)",
critical: "var(--cpd-color-text-critical-primary)",
alert: "#FF5B55",
links: "#0086E6",
"grey-25": "#F4F6FA",
"grey-50": "#E3E8F0",
"grey-100": "#C1C6CD",
"grey-150": "#8D97A5",
"grey-200": "#737D8C",
"grey-250": "#A9B2BC",
"grey-300": "#8E99A4",
"grey-400": "#6F7882",
"grey-450": "#394049",
},
fontWeight: {
semibold: "var(--cpd-font-weight-semibold)",
medium: "var(--cpd-font-weight-medium)",
regular: "var(--cpd-font-weight-regular)",
},
},
variants: {
extend: {},
},
plugins: [],
};
+3
View File
@@ -10,6 +10,7 @@ import path, { resolve } from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import zlib from "node:zlib";
import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import browserslistToEsbuild from "browserslist-to-esbuild";
@@ -217,6 +218,8 @@ export default defineConfig((env) => ({
react(),
tailwindcss(),
augmentManifest(),
compression(),
+222 -426
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -23,6 +23,7 @@ Please see LICENSE files in the repository root for full details.
<meta charset="utf-8">
<title>{% block title %}{{ _("app.name") }}{% endblock title %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ include_asset('src/entrypoints/vendor.css') | indent(4) | safe }}
{{ include_asset('src/entrypoints/shared.css') | indent(4) | safe }}
{{ include_asset('src/entrypoints/templates.css') | indent(4) | safe }}
{{ include_asset('src/entrypoints/templates.ts') | indent(4) | safe }}