Skip to content

refactor(web): migrate styled-components to Tailwind#1873

Draft
tyler-dane wants to merge 30 commits into
mainfrom
refactor/styled-components-to-tw
Draft

refactor(web): migrate styled-components to Tailwind#1873
tyler-dane wants to merge 30 commits into
mainfrom
refactor/styled-components-to-tw

Conversation

@tyler-dane

@tyler-dane tyler-dane commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What

Closes #1629

Completes the migration of packages/web off styled-components onto a hybrid Tailwind system, and removes the dependency entirely.

  • Styling system: inline Tailwind utilities for one-off layout/state; c-* recipes (Tailwind v4 @utility in packages/web/src/index.css) for reusable component recipes and complex third-party selectors.
  • Theming-ready: all theme-dependent colors are semantic CSS-variable tokens under @theme/:root, so a future [data-theme="light"] (light/dark) rollout needs no component changes. Inline CSS custom properties are used only for runtime values (event colors, positions, dynamic grid counts).
  • Dependency removed: no styled-components imports, type augmentation, Babel plugin, package dependency, or lockfile entry remain. A guard test (packages/web/src/common/styles/no-styled-components.test.ts) enforces this going forward.
  • Docs: .cursorrules/ updated to describe Tailwind as the sole styling system and document the c-* convention.

Why

styled-components added a runtime CSS-in-JS layer and a ThemeProvider dependency that made a semantic-token-based theming rollout awkward. Tailwind + semantic CSS variables keeps reusable recipes named while moving one-off styling local, and sets up light/dark theming without re-touching components.

Notable fix for reviewers

The migration had dropped role="form" when replacing <StyledEventForm role="form"> with a bare <form>. A <form> without an accessible name exposes no ARIA form role, so getByRole("form") stopped matching and silently broke 19 event-form e2e specs (unit tests query by placeholder, so they stayed green and hid it). Restored role="form" on both the timed and someday event forms — see 59091ce46. Takeaway: semantic attributes (role, aria-*, title) are load-bearing, not presentation; preserve them when restyling.

Verification

  • test:web ✅ 1085 pass (incl. styling guard + characterization tests)
  • test:core ✅ 145 pass
  • test:e2e50 pass (was 19 failing before the role="form" fix)
  • type-check ✅ · lint ✅ (27 pre-existing, non-blocking a11y/noSvgWithoutTitle warnings) · build:web ✅ · build:backend

Not covered

  • test:backend / test:scripts were not run — they require local Compass config/env (MONGO_URI, SUPERTOKENS_KEY, etc.) absent from this environment, and are unrelated to the web styling change. They run in CI.

🤖 Generated with Claude Code

tyler-dane and others added 30 commits June 18, 2026 16:49
The styled-to-tailwind migration replaced <StyledEventForm role="form">
with a bare <form>, dropping the explicit ARIA role. A <form> without an
accessible name exposes no "form" role, so getByRole("form") stopped
matching and broke the event-form e2e flows (19 specs). Restore role="form"
on both the timed and someday event forms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Describe Tailwind as the sole styling system and document the hybrid
inline-utility + c-* recipe convention. Note that semantic attributes
(role, aria-*) must be preserved when restyling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the internal design spec so it is not carried into the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reminder feature was deleted (#1875); its c-reminder-* utilities and
keyframes in index.css had no remaining consumers. Remove 7 utilities and
4 keyframes (~138 lines).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace single-use c-* layout recipes with inline Tailwind utilities,
removing the global-CSS indirection for styling used in exactly one place:
c-floating-form-container, c-calendar-main-grid, c-calendar-grid-rows,
c-calendar-timed-columns, c-week-columns, c-week-grid-track.

No visual change; theme tokens and semantic attributes unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert single-use c-someday-event and c-someday-recurrence-value recipes
to inline Tailwind utilities using data-[...]/hover: variants. Keep
c-week-edge-zone (its dual data-position gradient variants are a coherent,
complex treatment better expressed as a named recipe).

No visual change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Push descendant-selector recipes onto their child elements and inline the
remaining single-use calendar grid layout:
- c-week-day-labels: clamp font sizes moved onto the Text children
- c-calendar-grid-row: inlined (its '& > span' rule was dead)
- c-calendar-day-times: child height/display moved onto mapped children
- c-calendar-now-line: inlined (trivial)
- c-calendar-all-day-columns: inlined; pseudo grid-line via before: variant

Keep c-calendar-date-column (2 consumers) and c-week-edge-zone (gradient
variant set). No visual change; verified with web unit + 50/50 e2e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document that inline is the default and a c-* recipe is justified only when
inline can't express it (third-party descendant selectors, pseudo-elements,
keyframe bundles, or genuine reuse). Refresh stale examples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move single-use (or feature-local) recipes out of index.css to their call
sites as inline Tailwind, trading minor duplication for a leaner global
stylesheet: c-actions-menu(-item), c-recurrence-row/weekday/interval/caret,
c-week-grid-scroller, c-week-edge-zone, c-calendar-date-column. Pseudo-element
and data-state styling use arbitrary/data- variants.

Kept c-time-picker and c-planner-month-picker as global recipes: they style
third-party DOM (react-select/react-datepicker) that can't take Tailwind
classes, and the test harness already special-cases them.

Verified: web unit (1041) + 50/50 e2e + type-check + lint + build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single-use button recipe with no descendant selectors; inline at its call
site in NotFound.tsx and drop from index.css.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The styled-components migration moved the timepicker overrides into an
@Utility (c-time-picker), which Tailwind places in @layer utilities.
react-select injects its default styles unlayered at runtime, and unlayered
rules beat any cascade layer regardless of specificity — so the dropdown
rendered with react-select's defaults.

Make .c-time-picker a plain (unlayered) rule so it competes with react-select
on specificity again, as the original styled-component did. Verified the menu
background now resolves to the themed --time-picker-bg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant