Upgrade frontend to Tailwind CSS v4 - #5354
Merged
norman-abramovitz merged 10 commits intoMay 19, 2026
Merged
Conversation
Slice 1 baseline anchor captured at this SHA before any v4 changes. Pre-upgrade artifacts archived to KS/stratos/baselines/2026-05-19-tailwind-v4-slice1/: - v4-baseline-summary.md (gate, build, bundle numbers) - v4-baseline-sha.txt - v4-baseline-check.log (lint + vitest + go) - v4-baseline-build.log (release cf zip) - screenshots/ (13 Playwright captures, light + dark) Headline comparison anchor: styles-*.css = 104.34 kB raw, 14.35 kB transfer. v4 with Lightning CSS is expected to shrink it.
Codemod bumped tailwindcss 3.3.0 -> ^4.3.0 (resolved 4.3.0). Other migration work is manual: - @tailwind directives in .scss files (codemod scans CSS only) - postcss.config.cjs (dynamic JS, codemod skipped) - tailwind.config.js translation to @theme blocks - @apply in component SCSS (38 files) build-dev fails as expected: "tailwindcss directly as a PostCSS plugin... install @tailwindcss/postcss" Codemod log: /tmp/v4-codemod.log First-build log: /tmp/v4-first-build.log Follow-up tasks in plans/2026-05-18-slice-1-tailwind-v4-upgrade.md
Four changes that together unblock the v4 build:
1. postcss.config.cjs uses @tailwindcss/postcss (v4 plugin
moved to a separate package). v3 helpers tailwindcss/nesting
and autoprefixer dropped; v4 has native CSS Nesting and
Lightning CSS handles vendor prefixes.
2. @tailwindcss/postcss@^4.3.0 added to devDependencies.
3. main.scss directives: replaced
@tailwind base; @tailwind components; @tailwind utilities;
with
@import 'tailwindcss';
File stays .scss (uses %placeholder + @extend, SCSS-only
features). Sass passes the @import through to PostCSS.
Sass emits a deprecation warning about @import which is a
future-Dart-Sass-3 concern, non-blocking.
4. tailwind.config.js renamed to tailwind.config.v3-legacy.js
so @angular/build does not auto-detect it as v3 and force
the v3 tailwindcss package as a PostCSS plugin (which
throws "tailwindcss directly as a PostCSS plugin"). Task 5
reads this file to translate its theme.extend into a
@theme block in main.scss, after which it is deleted.
bun run build-dev now exits 0 with 0 errors (11.4s, dev mode).
Build log: /tmp/v4-after-main-rename.log
Plain CSS audit (4 plain .css files in src/, 243 SCSS files
without @apply): all Tailwind-independent, no conversion
required. See plan task 4 for details.
Moves v3 theme.extend entries into a @theme block at the top of main.scss right after @import 'tailwindcss'. Functional parity: every utility v3 generated still resolves under v4. Translated: - 6 color scales (brand, accent-shade, success-shade, warning-shade, danger-shade, info-shade) and their defensive singleton aliases (primary, accent, success, warning, danger, info, error) — 10 steps each - 8 semantic singleton colors (--color-primary etc., static LIGHT values; .dark-theme overrides at runtime so the same custom property name keeps working in 77+ direct var() references) - 1 neutral color (tentative) - 31 layout semantic tokens (nav-*, app-*, body-*, header-*, content-*, login-*, logo-bg, input-*) as --color-* aliases of the runtime --x tokens (no self-cycle — distinct names) - 3 background images - fontFamily (sans/mono), 10 fontSize entries with line-heights - 35 spacing scale entries - 9 borderRadius + 11 boxShadow entries - 7 ease (transitionTimingFunction) entries - 16 animation utilities + 14 @Keyframes blocks at root - 6 breakpoints Stratos behaviour preserved: - darkMode: 'class' as @custom-variant dark (&:where(.dark, .dark *)) - 8 redundant --color-{singleton} declarations dropped from :root block (now declared by @theme, which also emits at :root) - .dark-theme block unchanged — still overrides --color-* at runtime Deferred to Task 6: - safelist (progress bar / scroll gradient classes) - content paths (Angular detects most automatically) - plugins (@tailwindcss/forms, @tailwindcss/typography) - container customisation - v3 entries that match v4 defaults (borderWidth, fontWeight, letterSpacing, lineHeight, transitionDuration, maxWidth, opacity) carried no translation work - zIndex named entries (dropdown/sticky/modal/etc.) — only if templates actually use them build-dev: 0 errors, 11.8s, initial 7.20 MB (parity with Task 4). v3-legacy config file kept for Task 6 reference. Log: /tmp/v4-after-config.log
@tailwindcss/forms (with strategy: class) and @tailwindcss/typography move from JS plugins[] to v4 @plugin directives in main.scss. Both packages already declare v4 peer compatibility; only the load mechanism changes. The v3 safelist (12 entries protecting progress-bar colors and the scroll-shadow gradient from JIT pruning) becomes four @source inline() directives with brace expansion. Verified the classes appear in the compiled CSS bundle. Skipped translations (with reasons documented): - darkMode: already handled by @custom-variant in the Task 5 commit - content paths: Angular's PostCSS pipeline feeds source files to @tailwindcss/postcss directly, no @source directories needed - container customisation: no template uses the tailwind container utility - zIndex named entries (dropdown / sticky / modal / popover / tooltip / toast): no template references them - borderWidth, fontWeight, letterSpacing, lineHeight, transitionDuration, maxWidth, opacity: all v3 entries match v4 defaults tailwind.config.v3-legacy.js retired — every entry is either translated, deferred with reason, or skipped as default-matching. The full v3 config remains in git history at f0fa842^:tailwind.config.js. build-dev: 0 errors, 7.20 MB initial (parity), all 12 safelist classes present in compiled CSS. Log: /tmp/v4-after-plugins.log
Tailwind v4 requires every CSS/SCSS file that uses @apply outside the main entry to declare which Tailwind context to resolve against. The @reference directive imports those variables without re-emitting base/components/utilities — it's the v4 replacement for the implicit "@apply just works" of v3. Sweep covers all 37 component SCSS files using @apply across four packages (cf-autoscaler 2, cloud-foundry 8, core 12, kubernetes 15) plus one root SCSS file (core/src/styles.scss). Special cases: - custom-button-toggle.component.scss had @tailwind components; at line 1, replaced with @reference 'tailwindcss'; (the @tailwind directive is v3-only and would no-op under v4). - core/src/styles.scss and setup-welcome.component.scss have @use rules at the top; @reference moved AFTER @use because Sass requires @use before any other rule. main.scss is the global entry point with @import 'tailwindcss' and the @theme block; component files attach to that context via @reference. build-dev: 0 errors, 10.4s, initial 7.20 MB (parity). Log: /tmp/v4-after-apply-sweep2.log
Two v4 changes required template-side renames: 1. flex-grow / flex-shrink utility names dropped the "flex-" prefix. Affected 18 files (templates + 1 SCSS @apply): flex-grow-N -> grow-N flex-shrink-N -> shrink-N Mechanical sed; CSS property uses (flex-grow: 0; etc.) left untouched because they have a colon, not a dash. 2. *-opacity-N utilities removed in v4; values move to slash notation on the color utility itself. 13 occurrences across 7 files, rewritten in context: bg-black bg-opacity-50 -> bg-black/50 bg-white bg-opacity-N -> bg-white/N text-white text-opacity-90 -> text-white/90 ring-1 ring-black ring-opacity-5 -> ring-1 ring-black/5 bg-primary ... hover:bg-opacity-90 -> bg-primary ... hover:bg-primary/90 focus:ring-white/30 focus:ring-opacity-50 -> focus:ring-white/30 (the /30 already specified the opacity; ring-opacity-50 was redundant) Skipped intentionally: - shadow rename chain (v4 renamed v3 default shadow-sm -> shadow-xs, etc.) - stratos's @theme block defines custom shadow values for xs/sm/md/lg/xl/2xl, so the v3 names still resolve to the same hex in v4. No template changes needed. - bare `ring` utility (v3 default 3px -> v4 default 1px): grep found no occurrences in templates. build-dev: 0 errors, 12.3s, 7.20 MB initial (parity). Log: /tmp/v4-after-utils.log
Three independent v4 misconfigurations were silently failing without breaking the build: 1. @angular/build's postcss config loader only recognises `postcss.config.json` and `.postcssrc.json` — `.cjs` was never loaded, so @tailwindcss/postcss never ran in the prod (esbuild) pipeline. Vite (dev) did load `.cjs`, but with no plugin normalisation it surfaced as `Invalid PostCSS Plugin at plugins[0]`. 2. Sass inlined `@import 'tailwindcss'` from main.scss textually before PostCSS saw the file, so even if PostCSS had run, v4's content scanner would not have engaged on the inlined output. 3. main.scss's `@layer components` blocks used `@apply` directly with no `@reference 'tailwindcss'`, which would silently no-op once the pipeline did run. Fix: - Split tailwind.css out as a pure CSS entry: @import 'tailwindcss', @plugin, @source, @custom-variant, @theme, @Keyframes. - main.scss keeps SCSS-only content and now opens with `@reference 'tailwindcss'` so @apply resolves without re-emitting utilities (same pattern already applied to the 38 component SCSS files in `65281f40c8`). - Replace postcss.config.cjs with postcss.config.json using the object-map plugin form — both Vite and @angular/build accept it. - Wire tailwind.css ahead of main.scss in angular.json's styles array. Verified: dev styles.css went from 65.5 kB (no utilities) to 154.4 kB with 697 generated utility selectors; @theme transforms to :root tokens; @tailwind utilities placeholder is expanded.
The endpoint-list sort used raw `<`/`>` on the extracted string field, which produces ASCII order: uppercase 'K' sorts before lowercase 'k', and "Kevin 10" sorts before "Kevin 2". Replace with localeCompare using sensitivity 'base' (case-insensitive) and numeric: true (natural number ordering), so the user sees "Kevin, Kevin 2, kevin 3, Kevin 4" instead of "Kevin, Kevin 2, Kevin 4, kevin 3".
norman-abramovitz
approved these changes
May 19, 2026
norman-abramovitz
left a comment
Contributor
There was a problem hiding this comment.
walked through cloud foundry and other than minor differences. This phase of the conversion is complete. The next phase will do some more sccs to tailwind V4 conversions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tailwindcss@^4.3.0,@tailwindcss/postcss@^4.3.0).main.scssto a pure-CSStailwind.cssand switchespostcss.config.cjs→postcss.config.json— the only formats@angular/buildactually loads. This is the load-bearing change that makes v4 utilities emit at all.@themetoken surface emitted as:rootcustom properties.Closes #5351
Changes
v4 codemod + config translation
tailwind.config.jstheme.extend(colors, fonts, sizes, radii, shadows, breakpoints, animations) into a single@theme {}block.plugins: [require('@tailwindcss/forms'/'typography')]with@plugindirectives; v3safelistwith@source inline(); deletedtailwind.config.v3-legacy.js.PostCSS pipeline rewiring
tailwind.css(pure CSS) entry containing@import 'tailwindcss',@plugin,@source,@custom-variant,@theme,@keyframes. Wired ahead ofmain.scssinangular.jsonstyles array.main.scssretains the SCSS-only content (:root,.dark-theme,@layer base/components/utilities) and opens with@reference 'tailwindcss'so@applyresolves without re-emitting utilities. Same pattern as the component SCSS sweep.postcss.config.cjs→postcss.config.jsonwith the object-map plugin form.@angular/buildonly loadspostcss.config.json/.postcssrc.json(seenode_modules/@angular/build/src/utils/postcss-configuration.js:15); a.cjsfile is silently ignored, which is what caused the v4 pipeline to no-op until this PR.Per-file SCSS migration
@reference 'tailwindcss';to the 38 component SCSS files that use@apply.Deprecated utility renames
flex-grow-N→grow-N,flex-shrink-N→shrink-N(template-only, via sed; CSS property uses untouched).bg-*-opacity-N/text-*-opacity-N/ring-*-opacity-Nrewritten per-occurrence to slash notation.shadow-smchain not renamed because stratos's@themeblock preserves v3 shadow values under v3 names.Incidental fix found during visual sweep
</>→ uppercase 'K' sorted before lowercase 'k'. Replaced withlocaleCompare(sensitivity: 'base', numeric: true).Test plan
bun run lint— 0 errors, 104 pre-existing warningsbun run test— vitest suites passmake build frontend(production) — 0 errors, styles bundle 120.37 kB (16.20 kB transfer), 16.5 smake check e2e— 63 passed / 10 failed / 41 skipped / 3 did not run. All 10 failures are pre-existing test rot on develop (scroll-shadow+endpoints-register-modaltarget selectors from the legacy<app-list>BEM structure or the empty-state component; the apps + endpoints pages have used<app-signal-list>since the V2→V3 cutover). Tracking in the V2→V3 cleanup.https://localhost:5540renders correctly aftermake devfrom a fresh checkout (bun installafter pulling —postcssand@tailwindcss/postcssare new direct deps).Notes for reviewers
@tailwindcss/postcssrunning at all. The output looks plausible (default theme tokens + base resets inlined fromnode_modules/tailwindcss/index.css) but contains zero utility classes. Verifying utility presence is the quickest sanity check after any future change to the PostCSS or stylesheet wiring:grep -c '@tailwind utilities' dist/frontend/browser/styles-*.cssshould return 0, not 1.@import 'tailwindcss'deprecation warning during build (Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0) is benign and originates from inside ourtailwind.css's upstream-inlined content — it'll go away when Dart Sass 3 drops@import. The actual Tailwind entry is.css, not.scss, so we don't need to act on it now.