Skip to content

Migrate from Mantine to Tailwind CSS v4 + shadcn/ui#730

Open
MaxGhenis wants to merge 17 commits intomainfrom
tailwind-shadcn-migration
Open

Migrate from Mantine to Tailwind CSS v4 + shadcn/ui#730
MaxGhenis wants to merge 17 commits intomainfrom
tailwind-shadcn-migration

Conversation

@MaxGhenis
Copy link
Contributor

@MaxGhenis MaxGhenis commented Feb 23, 2026

Summary

Complete migration from Mantine UI to Tailwind CSS v4 + shadcn/ui (radix primitives). Zero @mantine/* imports remain. All Mantine packages removed from dependencies.

Tracking issue: #729

What changed

Foundation:

  • Tailwind CSS v4 with tw: prefix
  • shadcn/ui components (dialog, select, tabs, sheet, accordion, etc.)
  • cn() utility (clsx + tailwind-merge)

Wrapper components (Mantine API compatibility):

  • 6 layout components: Text, Stack, Group, Title, Container, Spinner
  • 3 hooks: useMediaQuery, useViewportSize, useDisclosure

Component migration (~190 files):

  • All static/presentational, home, blog, footer, column components
  • Sidebar, report output, household builder, flowView components
  • Charts, visualization, geographic selectors
  • Replaced @mantine/dates (YearPickerInput, DatePickerInput) with native HTML inputs

Removed:

  • @mantine/core, @mantine/dates, @mantine/hooks dependencies
  • eslint-config-mantine, postcss-preset-mantine dev dependencies
  • MantineProvider from app shells, test fixtures, Storybook
  • Mantine theme files (theme.ts, styles/colors.ts, styles/components.ts)

Code quality:

  • All 53 pre-existing ESLint errors fixed (unused imports, a11y, button types)
  • All TypeScript errors resolved
  • Code simplification pass on migrated components

Test plan

  • bunx vitest run — 2999 tests pass (248 files)
  • bun run typecheck — 0 errors
  • bun run lint — 0 errors (ESLint + Stylelint)
  • bun run build — succeeds (website + calculator)
  • All CI checks pass
  • Vercel preview deploys succeed
  • Visual smoke test on preview URLs

🤖 Generated with Claude Code

@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
policyengine-app-v2 Ready Ready Preview, Comment Feb 24, 2026 5:16pm
policyengine-calculator Ready Ready Preview, Comment Feb 24, 2026 5:16pm

Request Review

MaxGhenis and others added 6 commits February 23, 2026 08:19
Resolve 53 ESLint errors (unused imports, duplicate imports, missing
button type attributes, a11y keyboard handlers) and 3 stylelint hex
color warnings across 37 files. Run Prettier on all changed files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI was failing because the lockfile format changed between bun 1.2.0
and 1.2.21 (configVersion field removed). Update packageManager to
match the version used to generate the lockfile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unsupported `variant` prop from Container/Title components
- Change "flex-start" to "start" for Stack/Group align prop
- Replace pixel string gap values with token keys (sm/md/lg)
- Replace `h` prop with style={{ height }} on Stack
- Remove `size` prop from Progress component
- Resolve duplicate Label identifier (Recharts vs shadcn)
- Fix step prop type on VariableInput
- Export SheetPortal/SheetOverlay from sheet.tsx
- Fix USPlaceSelector type mismatch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace @mantine/dates (YearPickerInput, DatePickerInput) with native
  HTML inputs in 3 value setter components
- Remove MantineProvider from WebsiteApp, CalculatorApp, test fixtures,
  test utils, and Storybook config
- Delete Mantine theme files (theme.ts, styles/colors.ts, styles/components.ts)
- Remove @mantine/core, @mantine/dates, @mantine/hooks, eslint-config-mantine,
  postcss-preset-mantine from package.json
- Inline ESLint rules previously provided by eslint-config-mantine
- Update postcss config to remove mantine-breakpoint variables

Zero @Mantine imports remain. All tests pass (2999), typecheck clean,
build succeeds, lint clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code simplification from code-simplifier review:
- Fix nested ternary in MultiButtonFooter
- Simplify redundant style declarations in SidebarNavItem
- Convert arrow function components to function declarations
- Remove unnecessary fragment wrappers
- Simplify PathwayView containerContent variable
- Simplify IngredientSubmissionView badge rendering
- Clean up HomeHeader and HeaderBar inline styles
- Add displayName to React.memo components
- Format with Prettier

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use const with ternary and explicit string type annotation to avoid
TypeScript literal type narrowing conflict.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the title Add Tailwind CSS v4 foundation and migrate Phases 0-3 Migrate from Mantine to Tailwind CSS v4 + shadcn/ui Feb 23, 2026
@MaxGhenis MaxGhenis requested a review from anth-volk February 23, 2026 20:04
@MaxGhenis MaxGhenis marked this pull request as draft February 23, 2026 20:05
Tailwind v4 with prefix(tw) requires the prefix before the variant
(tw:lg:flex), not after (lg:tw:flex). The wrong ordering caused all
responsive breakpoints and state variants to silently fail, breaking
the header nav links, blog grid layout, footer layout, and hover
effects across the site.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shadcn Button default variant uses bg-primary which requires a
base --color-primary CSS variable. Added explicit teal styling to
the footer subscribe button to match the production appearance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@anth-volk
Copy link
Collaborator

We should talk about this PR. I don't fully understand the impetus for these changes, especially since Tailwind exhibits real drawbacks against Meantime and other front-end frameworks, which is why we ranked it low in our framework evaluation exercise last year. Also, this would introduce significant merge conflicts for important projects.

@MaxGhenis
Copy link
Contributor Author

MaxGhenis commented Feb 24, 2026

Thanks for the feedback Anthony. I'd like to understand the concerns better so we can have a productive discussion.

On the framework evaluation

You mentioned that Tailwind "ranked low in our framework evaluation exercise last year." I searched for this evaluation across GitHub issues/PRs (both app and app-v2), Slack (#app-redesign, #app-v2, #frontend, #design, #tech), Gmail, Granola meeting notes, and Google Drive — and couldn't find a written record of it. The closest I found was:

  • Feb 2025 (#app-redesign): I asked "any favorite design systems?" — you replied that "we have not already discussed design systems with our design vendor, nor have we done so internally" and "I also don't know if we've ever done enough research ourselves into design systems to definitively say what our preferences might be"
  • Aug 2025: Issues Integrate front-end design system into template site #11/Add styling #53 and PR Add styling #81 set up Mantine in app-v2, but no comparative evaluation was referenced

Could you share more details on this evaluation — when it happened, who participated, and where the results are documented? I want to make sure I'm not missing something.

On Tailwind's drawbacks

What specific drawbacks are you concerned about? The common ones I'm aware of:

  • Verbose class strings vs Mantine's prop API
  • No built-in components (addressed by shadcn/ui, which we're pairing with Tailwind)
  • Requires CSS knowledge vs Mantine's abstracted props
  • Class string maintenance in diffs

Happy to discuss tradeoffs, but worth noting the popularity gap is significant: Tailwind has ~48M weekly npm downloads vs Mantine's ~1.4M (~35x), and 93k vs 31k GitHub stars. shadcn/ui (Tailwind's component layer) has 84k stars despite being much newer. This matters for ecosystem support, community resources, and AI code generation quality.

On merge conflicts

This is a fair concern. This PR was specifically scoped to avoid conflict with your open PRs — it only touches static/presentational pages (home, blog, footer, header) and shared components. The migration plan (issue #729) is phased to defer areas that overlap with #556 until it merges.

That said, a full migration would eventually touch your PRs. Here's the scope I've estimated:

PR Author New files importing Mantine Mantine component usages Conflict severity
#556 (report builder) Anthony 59 ~300 (Box, Text, Group, Stack, ActionIcon, Button, Tooltip, TextInput, Modal, etc.) High — entirely new feature, all Mantine-coupled
#627 (tools page) Anthony 11 ~40 (Box, Text, Group, Container, SimpleGrid, Badge, etc.) Medium — 11 new files
#639 (hero redesign) Nikhil 0 0 None — no new Mantine
#538 (app split) Sakshi 9 ~25, but includes MantineProvider root wiring Medium — structurally significant

The plan is: your PRs merge first on their current Mantine code, then a follow-up migration PR converts those files to Tailwind + shadcn. This is additive work but doesn't block or conflict with your current development.

Broader motivation: aligning our tools

The goal isn't just to swap Mantine for Tailwind in app-v2 — it's to converge all PolicyEngine frontend projects on a single styling framework. Right now we have:

  • app-v2: Mantine
  • State legislative tracker: Tailwind
  • OBBBA explorer: Tailwind
  • policyengine-slides: Tailwind
  • Other standalone tools: Tailwind

This fragmentation means design tokens, component patterns, and developer knowledge don't transfer across projects. I first tried vanilla-extract (#662/#665) as a way to improve styling while keeping Mantine components, but VE interfered with Plotly.js module loading and caused choropleth maps to render blank. Tailwind compiles to static CSS via a Vite plugin with no runtime, so it doesn't have that problem.

Tailwind is the natural convergence point since most of our tools already use it, and pairing it with shadcn/ui gives us the component library layer. Beyond that, Mantine's breaking upgrade cycle (v6→v7→v8) has been costly each time, while with shadcn you own the component source code and never face library-driven breaking changes.

- Fix About dropdown: use onSelect with navigate() instead of asChild+Link
  (Radix DropdownMenuItem prevents default on Link clicks)
- Country picker: add border, country code label, hover state
- Dropdown menus: add explicit tw:bg-white background
- Footer: tighten spacing, softer link colors, smaller social icons
- FooterSubscribe: remove left padding, constrain input width, zero margins

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@anth-volk
Copy link
Collaborator

Thanks for the detailed response, Max. Let me address each section.

On the framework evaluation

The evaluation document is here: https://docs.google.com/document/d/1efbBqJncYTWydD7t08w69GaTXwo3LUpHN_Jd1IMv43E/edit?usp=sharing

The document didn't evaluate Tailwind as a standalone option, but rather as part of HeroUI, a component framework built on top of it. I conducted the evaluation and then met with Mike, Sakshi, and Nikhil to discuss the findings. You're right that the February 2025 comment predated this — the evaluation happened afterward.

On Tailwind's drawbacks

One of the primary goals of the evaluation was to find a framework that was feature-rich and would ensure internal styling consistency. The evaluation found that while Tailwind is highly customizable, it requires significant hand-building for most components. It also requires direct CSS knowledge and provides limited abstraction compared to component-level frameworks. The concern was that these characteristics would likely lead to less internal design cohesion and longer development time compared to a more opinionated component library.

On the popularity comparison: npm download counts and GitHub stars are useful signals for ecosystem health, but they don't map directly to suitability for a given project. It's also worth noting that shadcn/ui has significantly fewer npm downloads than Mantine, yet it was selected here alongside Tailwind. That suggests the decision was driven by factors beyond popularity metrics, which I think is the right approach — but it cuts both ways when evaluating Mantine as well.

On merge conflicts

The merge conflict analysis in the comment is useful, but it focuses on open PRs against the current base. The more significant migration concern is what happens when we eventually merge the api-v2 branch into main. That branch is currently 33 commits ahead of main with 119 files changed. It would be helpful to see an assessment of the merge conflict impact for that integration, since it represents the largest upcoming merge and is likely to be affected by a framework swap.

On tool alignment

The tool alignment argument is the strongest case here, and worth taking seriously. One thing I'd want to understand better is how Tailwind addresses visual consistency across projects. Tailwind provides low-level utility classes, which gives developers significant flexibility in how they style components. The PR mentions shared design tokens, which is an important piece, but the question is whether that's sufficient on its own to ensure front ends share a cohesive visual design. A component library like Mantine enforces a degree of visual consistency by default, since developers work within the constraints of the library's design system.

This may be an open question — shared visual alignment across projects may or may not be a priority — but it would be good to clarify that as part of this discussion.

On Mantine's breaking upgrade cycle

Worth noting that Mantine follows semantic versioning, so major-version breaking changes are expected and by design. This is standard across the ecosystem — any actively maintained library with major-version bumps will introduce breaking changes. The migration cost is real, but it's not unique to Mantine.

On feature completeness

The PR description notes 6 wrapper components and 3 hook shims that replicate Mantine functionality — 9 compatibility layers in total, and this is only a partial migration. This suggests that the Tailwind + shadcn/ui combination requires additional implementation work to reach parity with what Mantine provides out of the box. As the migration continues into more complex areas of the codebase, it's worth considering whether the number of custom shims will grow and what the maintenance implications of that would be.

An open question

I'd like to better understand the full case for Tailwind beyond tool alignment. For example: are there specific technical benefits — performance characteristics, styling guardrails, developer experience improvements — that Tailwind provides over Mantine for this project? And for the other PolicyEngine projects that already use Tailwind, was that an intentional technical choice, or more of a default that Claude selected? Understanding that original rationale would help in evaluating whether Tailwind is the right convergence point or whether the existing usage is more incidental than intentional.

@MaxGhenis
Copy link
Contributor Author

Thanks for the thoughtful response, Anthony. A few replies:

On the evaluation

I read the doc — thanks for sharing. It's a solid comparison of component libraries, but it evaluated Tailwind only as a dependency of HeroUI, not as Tailwind + shadcn/ui (which is what this PR proposes). The shortlist was Chakra, Mantine, and HeroUI — this option was never compared. So I don't think we can say it "ranked low" when it wasn't evaluated in its current form.

On the "Claude default" question

You asked whether Tailwind in other PE projects was intentional or Claude's default. Honest answer: mostly Claude's default. But I think that's signal, not a problem. When I start a new project, I ask Claude "if we were building this from scratch, what would you use?" — and it consistently picks Tailwind because of the 40x larger ecosystem, more training data, and better generation quality. In an AI-first workflow, aligning with what the AI is best at is a feature, not a bug. We should be letting the AIs cook, not fighting their defaults.

On visual consistency

Design tokens in Tailwind (which we define in packages/design-system/) enforce consistency the same way Mantine's theme provider does — colors, spacing, typography, border radius are all centralized. shadcn components are built on Radix primitives with our tokens applied. The consistency comes from the token system, not the component library.

On the shims

Fair point. The 9 compatibility wrappers exist because this is a phased migration — they get removed as each area converts. They're scaffolding, not permanent debt. The alternative (big-bang migration) would be far riskier.

On Mantine's semver

True that breaking changes on major versions are expected. The difference is that with shadcn, we own the component source code — there are no library-driven breaking changes ever. We upgrade on our timeline, not Mantine's.

On the migration branch conflict

Good call — I'll add a conflict assessment for the api-v2 migration branch to the PR description.

On the broader motivation

The question I keep coming back to: if we were starting app-v2 from scratch today, what would we pick? I'm confident the answer is Tailwind + shadcn. Given that, migrating now while the codebase is still young is cheaper than doing it later. And it converges us with the state legislative tracker, OBBBA tools, slides framework, and every standalone calculator we've built.

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.

2 participants