Skip to content

Upgrade frontend to Tailwind CSS v4 - #5354

Merged
norman-abramovitz merged 10 commits into
cloudfoundry:developfrom
nabramovitz:feature/FWT-982-tailwind-v4
May 19, 2026
Merged

Upgrade frontend to Tailwind CSS v4#5354
norman-abramovitz merged 10 commits into
cloudfoundry:developfrom
nabramovitz:feature/FWT-982-tailwind-v4

Conversation

@nabramovitz

@nabramovitz nabramovitz commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Migrates the stratos frontend from Tailwind v3 to Tailwind v4 (tailwindcss@^4.3.0, @tailwindcss/postcss@^4.3.0).
  • Pivots the entry from main.scss to a pure-CSS tailwind.css and switches postcss.config.cjspostcss.config.json — the only formats @angular/build actually loads. This is the load-bearing change that makes v4 utilities emit at all.
  • Production CSS bundle: 104.34 kB → 120.37 kB (+15.4%, ~16.2 kB transfer); the delta is mostly the v4 @theme token surface emitted as :root custom properties.

Closes #5351

Changes

  1. v4 codemod + config translation

    • Ran the upstream v4 codemod.
    • Translated tailwind.config.js theme.extend (colors, fonts, sizes, radii, shadows, breakpoints, animations) into a single @theme {} block.
    • Replaced v3 plugins: [require('@tailwindcss/forms'/'typography')] with @plugin directives; v3 safelist with @source inline(); deleted tailwind.config.v3-legacy.js.
  2. PostCSS pipeline rewiring

    • Split a new tailwind.css (pure CSS) entry containing @import 'tailwindcss', @plugin, @source, @custom-variant, @theme, @keyframes. Wired ahead of main.scss in angular.json styles array.
    • main.scss retains the SCSS-only content (:root, .dark-theme, @layer base/components/utilities) and opens with @reference 'tailwindcss' so @apply resolves without re-emitting utilities. Same pattern as the component SCSS sweep.
    • postcss.config.cjspostcss.config.json with the object-map plugin form. @angular/build only loads postcss.config.json / .postcssrc.json (see node_modules/@angular/build/src/utils/postcss-configuration.js:15); a .cjs file is silently ignored, which is what caused the v4 pipeline to no-op until this PR.
  3. Per-file SCSS migration

    • Added @reference 'tailwindcss'; to the 38 component SCSS files that use @apply.
  4. Deprecated utility renames

    • flex-grow-Ngrow-N, flex-shrink-Nshrink-N (template-only, via sed; CSS property uses untouched).
    • 13 bg-*-opacity-N / text-*-opacity-N / ring-*-opacity-N rewritten per-occurrence to slash notation.
    • v3 shadow-sm chain not renamed because stratos's @theme block preserves v3 shadow values under v3 names.
  5. Incidental fix found during visual sweep

    • CF endpoint cards sorted with raw </> → uppercase 'K' sorted before lowercase 'k'. Replaced with localeCompare (sensitivity: 'base', numeric: true).

Test plan

  • bun run lint — 0 errors, 104 pre-existing warnings
  • bun run test — vitest suites pass
  • make build frontend (production) — 0 errors, styles bundle 120.37 kB (16.20 kB transfer), 16.5 s
  • Visual regression sweep — 13 baseline views captured against v4 build, all match v3 reference for layout / typography / spacing / colors (data differences only)
  • make 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-modal target 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.
  • Reviewers: confirm https://localhost:5540 renders correctly after make dev from a fresh checkout (bun install after pulling — postcss and @tailwindcss/postcss are new direct deps).

Notes for reviewers

  • The single most surprising thing during the upgrade was that the v4 codemod + config translation will compile clean and produce a bundle of approximately the same size without @tailwindcss/postcss running at all. The output looks plausible (default theme tokens + base resets inlined from node_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-*.css should return 0, not 1.
  • The Sass @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 our tailwind.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.

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 norman-abramovitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@norman-abramovitz
norman-abramovitz merged commit dad6ac2 into cloudfoundry:develop May 19, 2026
12 checks passed
@nabramovitz
nabramovitz deleted the feature/FWT-982-tailwind-v4 branch June 17, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade Tailwind CSS v3 to v4

2 participants