Skip to content

refactor: replace Bootstrap with Tailwind CSS and Base UI - #3196

Open
Soxasora wants to merge 104 commits into
stackernews:masterfrom
Soxasora:feat/redesign-replace-bootstrap
Open

refactor: replace Bootstrap with Tailwind CSS and Base UI#3196
Soxasora wants to merge 104 commits into
stackernews:masterfrom
Soxasora:feat/redesign-replace-bootstrap

Conversation

@Soxasora

@Soxasora Soxasora commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Part of #3190, supersedes #3126, #3187, and #3188.

This PR replaces Bootstrap with Tailwind CSS v4, Base UI primitives, and shared SN components while preserving the existing design.

  • converts Bootstrap layout, color, and utility classes across components, pages, editor, and wallet to Tailwind.
  • replaces React Bootstrap components with shared SN UI components built on Base UI
  • breaks down the Form system into dedicated components
    • react-select (multiselect) is replaced by Base UI Combobox
  • introduces canonical --sn-* design tokens for light, dark, and territory-branded themes, shared z-index stacking, mobile safe areas, and reusable component recipes.
  • replaces the global Sass bundle
    • with ordered CSS entry points:
      • tokens
      • Tailwind
      • base styles
      • app styles
      • KaTeX
      • editor text
    • removes Bootstrap, React Bootstrap, Sass, and all remaining SCSS.
  • places every CSS module in @layer components, allowing call-site utilities to override component defaults consistently.
  • TEMP: lint checks enforce module layering, statically discoverable Tailwind classes, token-based colors, and removal of Bootstrap variables.

Base UI brings keyboard navigation, focus management, portal positioning, and dismissal behavior for interactive components.
The most relevant changes include swipe-dismissable toasts and mobile drawer, improved form semantics and link editor.

Screenshots

tbd, even though the migration targets visual parity

Additional Context

This PR touches almost every single area that's displayed to stackers.
The most important areas to QA are forms, editor interactions, mobile navigation, wallet onboarding (new!), and territory-branded views (less priority).

This PR is not merge-clean. Code style, comments and docs will be cleaned/updated by a subsequent new cleanup PR.

Checklist

Are your changes backward compatible? Please answer below:

Yes, this PR only handles the styling implementation.

On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:

6/10, components were checked as they were migrated. Pending a full end-to-end regression pass.

For frontend changes: Tested on mobile, light and dark mode? Please answer below:

Yes, during component migration. Pending a full parity pass.

Did you introduce any new environment variables? If so, call them out explicitly here:

No

Did you use AI for this? If so, how much did it assist you?

Yes, substantially. It assisted with mechanical Bootstrap-to-Tailwind conversions, Base UI rewrites, token and cascade analysis, and follow-up fixes. The resulting changes were then reviewed and adjusted manually.

This PR lacks the slop/inconsistencies cleanup, a new PR will be created for that.


Note

High Risk
Broad front-end refactor touching forms, editor, navigation, and theming across nearly all user-visible surfaces; regressions in layout, focus, and form submission are likely without full QA.

Overview
This PR replaces Bootstrap and React Bootstrap across the app with Tailwind CSS v4 utilities, Base UI primitives, and shared @/components/ui/* building blocks, while aiming for visual parity.

Design system: Bootstrap/--bs-* and --theme-* tokens give way to canonical --sn-* tokens; dark mode switches from data-bs-theme to data-theme. Component CSS modules are wrapped in @layer components so Tailwind utilities can override defaults. README and docs/dev/styling.md document the new stack.

Forms: The monolithic components/form.js is removed and re-exported from a components/form/ package (inputs, checkboxes, copy helpers, date pickers, etc.), using Base UI for checkboxes/combobox-style controls and InputAddon instead of InputGroup.

UI behavior fixes bundled with the migration: Lexical link editor uses a anchored Popover instead of manual floating DOM; carousel arrow keys listen on a focused container (works inside dialogs); delete cache updates lexicalState/html; screenshot capture hides nav via [data-sn-navigation]; badges gain sr-only text and optional interactive popovers; portaled menu delete uses a dedicated confirm hook.

Naming cleanup: accordian-itemaccordion-item on Base UI Collapsible.

Reviewed by Cursor Bugbot for commit 0afa998. Bugbot is set up for automated code reviews on this repo. Configure here.

@socket-security

socket-security Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​base-ui/​react@​1.7.0721008993100
Addedtailwindcss@​4.3.31001008498100
Addedtailwind-merge@​3.6.01001008696100
Addedpostcss-preset-env@​11.3.29610010093100
Added@​tailwindcss/​postcss@​4.3.310010010098100

View full report

@Soxasora Soxasora linked an issue Aug 20, 2026 that may be closed by this pull request
@huumn

huumn commented Sep 3, 2026

Copy link
Copy Markdown
Member

It's like this in prod too:

Ah, my recreation attempt must've been poor.

Do you mean that it would be better if the (i) info icon were adjacent to the last word?

Yeah. My bad.

@huumn

huumn commented Sep 3, 2026

Copy link
Copy Markdown
Member

Have you tried QAing a production build? I got one finding from claude that implies, if true, that css minification might break some css rules.

The chip's padding/border/background/color live on the single-class .chip rule, which ties (same layer, same specificity) with ui/collapsible's .trigger { border: 0; padding: 0; background: none; color: inherit; font: inherit } now composed onto the same button. The two-class rule the comment relies on only sets custom properties and min-height, so it arbitrates nothing. In dev, style-loader injects .trigger first (via _app) so .chip wins; in prod, mini-css-extract's DFS order over the send page visits wallet.module.css at index 0 and collapsible.module.css at index 24, so .trigger is emitted later and wins.

Also, I've attached some simplification/efficiency/latitude stuff that I haven't verified and might be relevant for #3216

p3 findings

P3-1 — latent traps (author backlog)

  • components/form/submit-button.js:16 — variant={variant || 'main'} maps to no .main rule; a caller omitting variant gets a transparent borderless submit button (all current callers pass one; both multi-line callers in pages/invites/index.js:108 and send-footer.js:15 do). Parity with master's equally undefined btn-main.
  • styles/tailwind.css:16 — @source not inline('container')/('table') block two real utilities (className='table w-full' emits nothing); they date from Bootstrap coexistence (964dd81). verifier: PLAUSIBLE/low; the bip39 diagnosis was refuted by a real compile.
  • components/form/input.js:100 — undefined Formik value at mount stays uncontrolled forever under Base UI useControlled; no current caller does this (C1).
  • components/ui/toast.js:74 — toaster.danger() timeout pinned to 0 regardless of options (B2).

P3-2 — duplication the shared modules already solve

  • components/ui/popover.module.css:33 (+ alert.module.css:39, drawer.module.css:65, modal.module.css:44, toast.module.css:166, wallets send.module.css:56) — lightning-font "X" close button drawn six times; modal/toast X have no focus ring; drawer uses ::after. (reuse, high)
  • components/login-button.js:91, components/editor/plugins/toolbar/index.js:116 — menu items skinned with legacy components/dropdown.module.css (.dropdownExtraItem, new [data-highlighted]) instead of ui/menu MenuItem/itemClasses; two active/highlighted paints. (reuse)
  • components/ui/menu.module.css:11, popover.module.css:13, tooltip.module.css:17 — popup motion block copied verbatim three times. (reuse)
  • components/new-header.js:22 — hand-rolled Base UI checkbox via checkbox.module.css instead of <Checkbox noForm>; lacks mb-0.5 and disabled/invalid treatment. (reuse)
  • components/territory-branding.js:53, components/territory-form.js:275 — hand-rolled form groups instead of FormGroup; nsfw label unassociated with its checkbox and doubles the group margin. (reuse)
  • components/form/range.js:88 — re-implements InputAddon with a second .addon paint rule that already drifted (hardcoded .375rem radius, no colour; renders 6px corners inside capability-card's 12px radius). (simplification, high)
  • components/form/select.js:10 (multi-select.js:16, checkbox.js:19, date-picker.js:23, otp-input.js:11) — bypass useFormikField and recompute un-gated invalid; same split as master, not a regression. (altitude)
  • components/form/use-field-draft.js — 9-line module with one consumer; draft key format duplicated in form.js:33. (simplification)

P3-3 — dead weight

  • components/text.module.css:53, lib/lexical/theme/editor.module.css, plus item/comment/reply/adv-post-form/upvote/user-header/payIn-table modules — ~590 lines of module CSS with no JS consumer re-indented under @layer components. (simplification)
  • components/ui/toast.js:87 — useToast exposes add/close/update/promise, progressBar, persistOnNavigate, autohide, delay, onRemove, id; all 78 call sites use success|warning|danger(body[, {tag}]); progressBar has zero callers and drags a countdown div + keyframes. (simplification)
  • components/ui/badge.js:6 badgeClasses; Client HOC; components/modal.js:9 ModalClosedError (never caught); barrel BaseSuggest/ClientInput/SessionRequiredError; isValidHexColor — exports with zero importers. (simplification)
  • styles/app.css:21 .justify-self-center duplicates the emitted utility; .pointer (app.css:80, 17 uses incl. components/ui/menu.js:47) and .small (app.css:270, 16 uses) survive beside cursor-pointer/text-sm in new code; file-upload.js:124 uses a third mechanism (style={{ cursor }}); disabled Combobox.Clear/Trigger keep a pointer cursor from the utility. (reuse/simplification/altitude)
  • components/ui/toast.js:57 — useToast()Toast.useToastManager() subscribes all ~43 consumers to the toast list; every add/measure/close re-renders them and re-runs Lexical registerCommand effects with toaster in deps (toolbar/switch.js, core/formik.js). Master returned a memoized context value. (A1/C2, efficiency)

P3-4 — altitude

  • components/ui/button.module.css:104-116 (.nostr fill/stroke !important, no svg consumer), components/form/field.module.css:111/124-126, wallets configure.module.css:63, home/rows.module.css:183-202, send/send.module.css:167/219-228 (.internalIcon unused), form/test-status.module.css:12 — Bootstrap-era !importants that now arbitrate nothing; all pre-existing. Caveat (verifier): .appendButton { border-left: 0 !important } is load-bearing against .clearButton { border }.
  • styles/tokens.css:113-125 — --wt-* radius/gap namespace consumed only by wallet modules that then own metrics; capability-card.module.css:69 rebinds --sn-input-radius: var(--wt-radius-md); transaction-detail.js:44 inline gap: var(--wt-gap-md). (altitude)
  • components/form/multi-select.module.css:37 — literal @media (max-width: 767px) beside max-md: utilities on the same widget. (reuse)
  • wallets/client/components/send/send.module.css:39 z-index: 2000; lib/lexical/theme/editor.module.css:528 (10000), :539 (2000), :347/:409 (1000); styles/dnd.module.css:16; components/thunderstorm/thunderstorm.module.css:12 — literal z-indices outside the --sn-z-* ladder (all pre-existing values). (reuse/altitude)
  • components/ui/nav.js:10 (+5 sites) nested cn() inside recipes; checkbox/radio hand-concatenate class strings; three files import both classnames and cn; nav/common.js uses classNames 14 times. (simplification)

@Soxasora

Soxasora commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Have you tried QAing a production build? I got one finding from claude that implies, if true, that css minification might break some css rules.

Tried today, minification produced the same behavior as unminified CSS. This is more a CSS ordering bug, .trigger as last removed the chip's padding and border, and also changed the font weight. Fixed this by using :where(.trigger) so that the classes on top win regardless of order.
Thank you, didn't notice this!

I'm saving P3 findings for #3216, lots of really good insights.


Fixed:

  • Info icon to the right of the Spark warning
  • No spacing around the @anon dropdown buttons
  • Horizontal scroll in the mentions dropdown
  • link editor popover stuck at its first position: f2b766c
    • Base UI memoizes the anchor, fixed by re-rendering the link editor on nodeKey (id of selected link node) changes
      linkeditor
  • passphrase show/hide and numbering labels: cddbb3f
    • bootstrap->tailwind codemod mistranslated text-base font size from the global form-control class
      passphrase
  • embed blocks with background: d140ee0
    • embeds were using the undefined --theme-bg var as background, which was fine until undefineds got swept and --theme-bg became --sn-body-bg, showing the background (not the only case of an undefined style coming back to life)
      image
  • avatar/nym spacing in switcher: a94bd79
    • Tailwind preflights img with max-width: 100%; height: auto, and the switcher avatars are plain img with width and height attributes. It got fixed by using revert-layer to cancel the preflight's declaration for img. It was also impacting the job form's placeholder img, the SN logo, etc.
      image
  • toasts not dismissing, separate counts: 2f81846
    • Toasts are now under a global Base UI toast manager, and counts now derive from Base UI's updateKey, resetting when the toast closes.
    • Also now toasts can be sent by code outside of React (but still part of the app) via the exported toaster
  • post badges too prominent: 8c0f5d8
    • restored Bootstrap's em sized badges
      image
  • Cowboy hat vertically misaligned: it seems the same as prod's, hmm we could bump it down 1px?
  • sticky navbar dropdown: 11d2085
    • previously, menus rendered via portal under body, causing the menu to lag behind the sticky bar or remain visible after the original navbar scrolled away.
    • MenuProvider renders sticky menus via portal inside the navbar, so they move and hide with it. It also closes the navbar's menu when switching between regular and sticky nav.
stickybar-menu.mp4

wip:

  • Territory combobox too big

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread components/nav/common.js

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0afa998. Configure here.

setEditedLinkUrl('')
if (isLinkEditMode) setIsLinkEditMode(false)
hideFloatingElem(false)
onDismiss()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Link popover dismisses on selection change

Medium Severity

$updateLink now calls onDismiss() whenever the current selection is not the tracked link. That races the plugin’s SELECTION_CHANGE_COMMAND handler, which resets dismissed and then updates nodeKey. The still-mounted editor sees the new selection against the old key and sets dismissed back to true, so the popover does not appear for the newly selected link.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0afa998. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate to TailwindCSS and Base UI

2 participants