Skip to content

feat(fuselage): render components with Tamagui and drop SCSS - #42942

Draft
ggazzo wants to merge 131 commits into
refactor/fuselage-tamagui-prepfrom
feat/fuselage-tamagui-pilot
Draft

ggazzo wants to merge 131 commits into
refactor/fuselage-tamagui-prepfrom
feat/fuselage-tamagui-pilot

Conversation

@ggazzo

@ggazzo ggazzo commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Proposed changes (including videos or screenshots)

40 Fuselage components now render with Tamagui (@tamagui/web 3.x). The package no longer contains or builds any SCSS, and Sass is gone from its build, Storybook and lint. Components look and behave as before.

Decisions, measurements, the migration recipe and the lessons learned are in docs/proposals/fuselage-tamagui.md.

How it fits Rocket.Chat

  • Colors and themes: no Tamagui tokens or themes. Values stay on the --rcx-* custom properties, so palettes, dark mode, high contrast and custom themes keep working.

  • Cascade layer: Tamagui styles sit in @layer fuselage. Unlayered CSS keeps overriding them: Box styling props (css-in-js), app CSS and admin Custom CSS.

  • Global element rules: the app's Meyer reset and its button / input / code / pre / blockquote / :focus rules move into @layer reset, so element selectors no longer win over the layered component styles.

  • Public API: props, rcx-* class names and ref forwarding are kept. State class names (is-hovered, focus focus-visible, …) still pin a component's state, through forceStyle.

  • DOM: kept as before, except decorative pseudo-elements, which became aria-hidden elements. Box props reach Tamagui frames through useTamaguiBoxProps.

  • What stays plain CSS (fuselage.css, flat CSS, no Sass): what Tamagui can't express — native :checked/:invalid, rich text, @keyframes, @font-face — plus the rcx-* class names other code puts on its own elements (styles/consumer-classes.css).

  • No provider required: Tamagui needs a theme context, so every component used to mount a fallback FuselageProvider. A patch now lets a frame with no ThemeProvider above act as its own root.

    • Tamagui's base stylesheet ships in fuselage.css. yarn tamagui-css generates it, and yarn lint fails when it is stale.
    • FuselageProvider is an optional ThemeProvider that only lets components share one root; the app mounts one at its root.
  • Layer order: @layer reset, fuselage; comes first in fuselage.css, and is also prepended to <head> before Tamagui inserts its runtime sheet. Pages that inject CSS from JavaScript (Storybook) would otherwise let element resets beat every frame.

  • Group clauses off the hot rows: a Tamagui group-* clause subscribes its element to the group, and the first subscriber makes the parent render again on mount. Message and Item reveal their parts (timestamp color, toolbar, hover-revealed actions) with plain rules in the fuselage layer instead. Mounting gets 26% faster for message rows and 43% faster for item rows with actions. The first update after a message mounts gets 17% slower, since it now warms Tamagui's caches; later updates are unchanged.

  • Compiled with the Tamagui compiler: the Fuselage build runs @tamagui/vite-plugin, so a frame rendered with literal props becomes a plain element with static classes, with no Tamagui component at runtime.

    • prevalStyled.ts evaluates each module's styled() options at build time, since the compiler only reads literals.
    • Extracted rules land in the fuselage layer.
    • 45 components that only style one element are now the frame itself (Message leaves and containers, Card parts, States, Sidepanel, ButtonGroup, NavBar, …), so an app that runs the compiler flattens their use-sites too. Their rcx-* classes come from always-on variants, so a consumer's className now adds to them instead of replacing them, as on develop.
    • PureItem and its parts: the Item frames with nothing worked out at runtime; the row declares its state, gap and highlight. Item also takes multiline and interactive, which skip its re-render after mount (200 sidebar rows mount 24% faster).
  • Bundling: @tamagui/web is bundled into the Fuselage dist with its runtime settings pinned, so consumers need no bundler config.

  • Patches: yarn patches cover Tamagui 3.0.18:

    • React 19 ref cleanups;
    • forceStyle and onLayout leaking to the DOM;
    • a stopPropagation() after every frame onClick;
    • frames that throw without a theme provider;
    • in the compiler (@tamagui/static): a frame's default attributes (role, aria-*) dropped when it flattens, and variants given a runtime value bailing out instead of compiling to a lookup over their classes.

    They stay until upstream handles them.

Commits

  • The first ten commits work on develop as is (no Tamagui) and can each be extracted into their own PR:

    • fix(fuselage): give the private message indicator its modifier class: a stray backtick in its class string;
    • fix(fuselage): stop adding an undefined class to the reaction counter;
    • perf(fuselage): register Item titles only for menu items: Every ItemTitle registered its id with state, so each row rendered twice on mount; only menu items read those ids.
    • refactor(fuselage): extract rcx-* prop class names from useBoxOnlyProps;
    • refactor: move the global CSS reset into a reset cascade layer and refactor: move the remaining global element rules into the reset layer: these change the app's cascade, so they need an app A/B against develop before they are extracted;
    • chore(fuselage): add a script comparing bundle size and render cost with develop;
    • refactor: render the asset upload control with Button;
    • fix(fuselage): don't read window when OwnerDocument loads;
    • fix: name the license drop zone's stroke colors as strokeColor expects.

    With only these ten on develop, Fuselage typechecks and all 494 tests pass.

  • The next nine are phase 0 of the migration, still without Tamagui: visual state is no longer forced or carried by class names (items 6.1–6.5 of docs/proposals/fuselage-phase-0-inventory.md).

    • Select open state comes from aria-expanded.
    • The InputBox wrapper's invalid and disabled looks come from :has().
    • Invalid and disabled selects use the rcx-select--invalid and rcx-select--disabled modifiers.
    • Options finds the active option by position.
    • The state showcases render through storybook-addon-pseudo-states.
    • The forced-state classes are gone; this is breaking, and has a changeset.
    • Fixed along the way: a disabled Select trigger that stayed enabled, a SelectLegacy that needed two clicks to reopen, and the close chat modal's subject field, which never showed a focus ring.

    On develop alone they typecheck, all 495 tests pass, and 553 of 561 stories are pixel-identical. The Select states story is the one real change: it now shows hover and focus.

  • The next 53 commits are the second phase-0 round, still without Tamagui, in four batches. They are recorded in docs/proposals/fuselage-phase-0-inventory.md.

    • Batches:
      • the Select family;
      • containers (Contextualbar, Modal, Card, Table, Tile);
      • feedback and controls (Slider, ToastBar, Throbber, AudioPlayer, Bubble, Chip, Tabs, Accordion);
      • Message, NavBar, Sidebar, Item and Option.
    • What they change:
      • a consumer className is merged instead of replaced or dropped;
      • props that were dropped are forwarded;
      • runtime css became static CSS with custom properties;
      • default looks moved into each component's classes;
      • the style token helpers are typed by the token lists.
    • Bugs fixed along the way: a disabled tab that stayed clickable, FramedIcon ignoring size, ToastBar dropping its id, SelectLegacy ignoring a controlled value, aria-multiselectable always true, a lost ref, Button dropping rel, and an AccordionItem that would not collapse.
    • Deferred, because each changed what users see:
      • container queries for Banner and Callout collapsed them in content-sized flex rows;
      • child selectors for CardGroup and FieldGroup changed the spacing of three forms.

    On develop alone the whole base typechecks and passes its tests, Meteor's tsc passes, and the Fuselage Storybook is pixel-identical to the base it started from, apart from known timing flakes. The pilot ports these batches onto the frames in its last four commits. The integrated pilot is pixel-identical to the previous pilot in 551 of 562 stories; the other 11 are timing flakes. Six forms that render custom fields are pixel-identical to develop.

  • Enabling refactors and fixes come next.

  • Then one commit per component.

  • Then shared CSS, toolchain and docs.

  • Some components depend on each other, so the PR is the unit; not every intermediate commit passes on its own.

Measurements

  • Component SCSS: 5,923 lines → none; about 2,650 lines of plain CSS remain.
  • fuselage.production.js: 34.4 → 96.5 kB gzip.
  • fuselage.css: 26.7 → 8.7 kB gzip.
  • Net about +44 kB gzip.
  • Compiled in an app (bench pages built with the compiler against an ESM Fuselage build), warm mount: 150 messages 16.1 → 10.1 ms, 300 button groups 11.8 → 1.5 ms, 200 sidebar rows with PureItem 1.7 ms against 38.5 ms with Item. Computed styles are identical at rest, hovered and focused.
  • Rocket.Chat doesn't run the compiler yet; that needs an ESM Fuselage build and the compiler in the app's bundler (rspack).

After rebasing on develop with #42974 (components take no styling props), #43021 (spacing scale), #43047, #43017 and #42962:

  • Ported onto the frames: the rebase kept the pilot's Tamagui version of each component, so five commits on top bring develop's changes over, one per pull request. refactor(fuselage): components take no styling props #42974 adds Button compact, ButtonGroup stretch='equal', Divider flush/light, Chip small/label, Table columns and row headers, Tabs stretch, Modal hidden, InputBox endAddonFlush and the rest. The CardGrid and rcx-tile rules that develop styles in SCSS are plain CSS here.
  • Fuselage Storybook against develop: 554 of 561 stories are pixel-identical. The other 7 (Tooltip arrow positioning, Dropdown, PositionAnimated, OptionsPaginated) differ in when they finish drawing, not in their styles: settled, they match, and they also differ between captures of the same build.
  • Checks:
    • Fuselage tsc and oxlint: 0 errors.
    • Fuselage Jest: 509/509.
    • Meteor tsc: 0 errors.
    • scripts/scan-style-props.mjs --max 1: passes.

After rebasing on #43100 (components no longer accept Box styling props) and #43087 (positioned popups):

  • Types: every Tamagui component now has the public props type refactor(fuselage): components no longer accept Box styling props #43100 gives it: ElementProps plus its declared variations, with className as a string. The frames also gain refactor(fuselage): components no longer accept Box styling props #43100's variations: TableCell truncate/color/strong, Table columns in px and percentages, ModalFooter justify, Modal maxHeight, Tile flush.
  • No styling props smuggled through casts: styles refactor(fuselage): components no longer accept Box styling props #43100 keeps in SCSS are plain CSS here (NavBar divider, thread follow icons) or go through useStyle (Callout, StatesIcon, ToastBar, CardGrid's row height).
  • Fuselage Storybook against develop: 551 of 561 stories are pixel-identical. The other 10 differ only in capture timing (Tooltip arrows, OptionsPaginated, PositionAnimated).
  • Checks:
    • Fuselage tsc: 0 errors.
    • Fuselage Jest: 509/509.
    • Meteor tsc: 0 errors.
    • scripts/scan-style-props.mjs --max 0: passes.
  • Next: useTamaguiBoxProps and withFuselageProvider({ takesBoxTransforms }) no longer have a styling prop to translate, so they can go. Most of the remaining components can then become exported frames the compiler flattens.

Issue(s)

https://rocketchat.atlassian.net/browse/ARCH-2565

Steps to test or reproduce

  1. Rebuild Fuselage. If yarn dev:next was already running, restart it after removing apps/meteor/node_modules/.vite; otherwise it serves the old Fuselage JS with the new CSS.
  2. Run the app, for example RC_SERVER_URL=https://open.rocket.chat yarn dev:next.
  3. Go through the home page, a room, the members and user panels, a modal, preferences and the marketplace.

What was compared against develop:

  • Fuselage Storybook: all 531 stories, pixel by pixel in the same Chromium. All identical.
  • apps/meteor and ui-client Storybooks: the only failures are stories that also differ between two develop builds.
  • The real app: the pages listed in step 3, logged in on open.rocket.chat, are pixel-identical to a develop-based build. The only differences are avatars still loading and the port number shown in the Site URL warning.
  • Jest:
    • Fuselage 669/669.
    • Meteor client: all 379 suites pass.
    • The fuselage-dependent packages' unit tests are green.
    • Snapshot diffs show only Tamagui class names, the removed rcx-box rcx-box--full, class order, shifted _r_N_ ids and the new aria-hidden decorative elements.
  • Types and lint: Meteor tsc and the fuselage-dependent packages' typecheck pass, and Fuselage lint has no errors.

After the provider-free change, compared against the commit before it:

  • Fuselage Storybook: 527 of 531 stories are pixel-identical. The other 4 (PositionAnimated, Dropdown) also differ between two runs of the same build.
  • Jest:
    • Fuselage: 669/669.
    • Rocket.Chat's Jest suites: 3,209 tests, all passing.
    • The fuselage-dependent packages' unit tests are green, apart from abac, which doesn't use Fuselage and needs a database.
    • Snapshots only shift _r_N_ ids, since each component calls useId once less.

After the group change:

  • Fuselage Storybook: 527 of 531 stories are pixel-identical. The other 4 (Tooltip positioning, PositionAnimated) also differ from run to run.
  • Computed styles: identical for message rows and item rows with actions, at rest, hovered and focused.
  • Jest:
    • Fuselage: 669/669.
    • Rocket.Chat: 3,559 tests.
    • Consumer packages: green. Snapshots only lose the t_group_* class names.
  • Not run here: customOAuth.spec.ts (server) crashes the Jest worker in this environment, as it did before the change.

After the compiler changes:

  • Fuselage Storybook: 525 of 531 stories are pixel-identical. The other 6 (PositionAnimated, Dropdown) also differ between two captures of the same build.
  • Compiled dist: the computed styles of the bench pages and the ui-client Storybook built against it match the uncompiled build (the ui-client failure, UserAutoComplete, fails in both).
  • Jest:
    • Fuselage: 675/675 before the rebase, 452/452 after it (the rest were snapshot tests).
    • Meteor client and the fuselage-dependent packages: green. Their snapshots only changed Tamagui atomic class hashes, class order and generated ids; BannedUsers also lost a stale t_group_rcx-item left from the group change.

develop removed the Jest snapshot tests (#42977). The branch is rebased on it and carries no snapshots; the snapshot comparisons above were made before that.

Style recalc on a room-like page (about 1,010 elements: sidebar rows, messages, toolbar, form; Chromium, median of 15 runs):

develop pilot
Recalc during load 1.94 ms 2.10 ms
Forced full recalc 2.39 ms 3.51 ms
CSS file used 5.5% of 256 kB 12.7% of 86 kB
  • Selector matching is a small part of it. Each element matches about 3.2× more rules (atomic classes and the .is_View/.is_Text base), and the cascade processes each.
  • Tamagui's .is_DOM base rules, which never match a Fuselage frame, are no longer emitted.
  • The item focus rules check :focus-within before :has(). Together these cut the full recalc from 3.87 to 3.51 ms.

Further comments

  • Bundle size: the runtime is still bundled; TAMAGUI_RUNTIME=island could drop most of it once enough use-sites compile. See docs/proposals/fuselage-tamagui.md.
  • Known leftover: MessageHeader and other components with logic still put their rcx-* class before the consumer's props, so a consumer className replaces it.
  • Box: Box itself still runs on css-in-js.
  • Out of scope: RaisedHandsButton in ui-conference spreads pressed into a Box and gets a React warning; judging from the code, develop does the same.

@changeset-bot

changeset-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2312d98

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@rocket.chat/fuselage Minor
@rocket.chat/meteor Patch
@rocket.chat/uikit-playground Patch
@rocket.chat/core-typings Patch
@rocket.chat/rest-typings Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dionisio-bot

dionisio-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is still a draft
  • This PR is missing the 'stat: QA assured' label
  • GitHub is still computing mergeability — this will refresh shortly

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ggazzo ggazzo added the preview Builds a UI preview that Rocket.Chat Desktop can load label Oct 20, 2026
@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.94156% with 874 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.49%. Comparing base (5c7b25f) to head (53cba58).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #42942      +/-   ##
===========================================
+ Coverage    72.16%   72.49%   +0.32%     
===========================================
  Files         5323     3352    -1971     
  Lines       228566   189594   -38972     
  Branches     42372    31207   -11165     
===========================================
- Hits        164944   137437   -27507     
+ Misses       58413    50734    -7679     
+ Partials      5209     1423    -3786     
Flag Coverage Δ
e2e ?
e2e-api ?
unit 72.49% <73.94%> (+0.25%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🖥️ UI preview

Built from 2312d98 and published to ghcr.io/rocketchat/rocket.chat-web:pr-42942.

In Rocket.Chat Desktop with Developer Mode on, open one of these links to load this PR's UI into the workspace in focus:

Latest build of this PR rocketchat://ui-preview?pr=42942
This exact build rocketchat://ui-preview?pr=42942&sha=2312d98825567c8ca2b6c9970f7b1714ee869e6e

To pick the workspace, add &host=https://your.server. Use View > Restore server UI, or restart the app, to go back.

Or run it in a browser against any server (default https://open.rocket.chat):

npx @rocket.chat/ui-preview 42942 --server https://open.rocket.chat

@ggazzo ggazzo added this to the 9.0.0 milestone Oct 20, 2026
@ggazzo ggazzo changed the title feat(fuselage): render components with Tamagui feat(fuselage): render components with Tamagui and drop SCSS Oct 20, 2026
@ggazzo
ggazzo force-pushed the feat/fuselage-tamagui-pilot branch from f6e25f2 to 92ef7df Compare October 20, 2026 17:57
@ggazzo
ggazzo force-pushed the feat/fuselage-tamagui-pilot branch 2 times, most recently from ce83ecc to 0035ee2 Compare October 21, 2026 01:16
@ggazzo
ggazzo force-pushed the feat/fuselage-tamagui-pilot branch 7 times, most recently from c505bd7 to 75c2ce4 Compare October 26, 2026 21:39
@ggazzo
ggazzo force-pushed the feat/fuselage-tamagui-pilot branch from 2189996 to 39b444c Compare October 26, 2026 21:44
Tamagui ships ESM only, and packages that test against Fuselage sources import it directly.
Tamagui registers global pointer listeners at import whenever `window` exists.
Pilot for moving Fuselage components to Tamagui (`@tamagui/web` 3.x).
Decisions, measurements and open questions are in
`docs/proposals/fuselage-tamagui.md`.

- `Divider` keeps its props, `rcx-divider*` class names and look; Box
  styling props still apply through css-in-js.
- Tamagui rules live in the `fuselage` cascade layer, so unlayered CSS
  (app, admin custom CSS, css-in-js) keeps overriding them.
- `@tamagui/web` is bundled into `dist` with its runtime settings pinned.
- `FuselageProvider` is optional; Rocket.Chat mounts one at the app root.
ggazzo added 26 commits October 26, 2026 23:08
…up clauses

A Tamagui group clause subscribes its element to the group's state, and the first subscriber makes the parent render
again on mount and attach hover and press handlers. Every message rendered three subscribers (timestamp, toolbar
wrapper, toolbar or reaction action) and every item row with hover-revealed actions two.

Those clauses are now plain rules in the fuselage layer (.rcx-message:hover .rcx-message-header__time and the like,
with hover inside @media (hover: hover) as Tamagui does), and Message, Item and ItemGroupHeader declare no group.

Measured on 150 message rows and 200 item rows with actions: mount is 26% and 43% faster. The first update after a
message mounts is 17% slower, since it is now the second render that warms Tamagui's per-instance caches; later
updates match. Computed styles match at rest, hovered and focused.

The collapsed Sidebar keeps its rcx-sidebar group: it has a handful of subscribers per page, not one per row.
…default attributes

@tamagui/vite-plugin 3.0.18 flattens a use-site of a styled() frame into a plain element, but on web it dropped the
frame's non-style defaults, such as role="group" on ButtonGroup: only the attributes written at the use-site reached
the element, while its native path already carried the defaults over. The patch does the same on web, letting a prop
written at the use-site still win. Nothing runs the compiler yet.
…values to a class table

The compiler flattened a frame only when every variant prop was a literal or a ternary of literals, so a row state
written as `selected={isSelected}` kept the whole element on the runtime. A variant can only take one of its keys or
none of them, so the patch treats it as a finite domain, as the compiler already does for a frontend's className: each
value resolves at build time and the element's className picks the classes by the runtime value
(`{"true": "...", "false": "", "undefined": ""}[isSelected]`). Class names a variant adds come along.
…piler

The Fuselage build now runs the Tamagui compiler, so every frame use-site inside the library that passes no spread
renders a plain element with static classes, such as a Button's inner content or a Tag's label: 70 elements, against
20 without `prevalStyled`.

- `prevalStyled` evaluates each component module at build time, with `@tamagui/web` stubbed, and rewrites the options
  of the frames such a use-site renders as JSON literals, since the compiler only reads literals in a module's own
  definitions.
- The extracted CSS goes in the `fuselage` layer, beside the runtime's rules.

Jest and Storybook still run `src`, so the compiled output was checked through `dist`: computed styles of the bench
rows match the uncompiled build at rest, hovered and focused, and the ui-client Storybook is pixel-identical. Mounting
gets 17% faster for Buttons, 25% for Tags and 6-7% for item and message rows; the bundle grows by 4.2 kB of JavaScript
and 3.1 kB of CSS (gzip).
…ad of finding them out after mount

An Item that is given multiline and interactive skips the registration that re-renders it once its content and link have mounted. A 200-row sidebar list mounts in 27 ms instead of 35 ms when warm and 48 ms instead of 63 ms when cold, with the same computed styles.
…their Tamagui frames

Forty-two wrappers that only set rcx-* classes and constant props on a single frame, plus ButtonGroup, ThreadMessageOrigin and MessageGenericPreviewDescription, which also map props to modifier classes, are now the frames themselves. The Tamagui compiler can flatten their use-sites in an app, and a consumer's className now adds to the rcx-* classes instead of replacing them (or being dropped, as States did).

The rcx-* classes come from always-on variants, since a className default would be replaced by the consumer's. The Fuselage Storybook is pixel-identical (525 of 531 stories; the other six differ from themselves between captures). With the app compiled, 150 messages mount in 10.1 ms instead of 16.1 and 300 button groups in 1.5 ms instead of 11.8, with the same computed styles.
PureItem and its parts (media, content, row, title, meta, description) are the Item frames with their rcx-* classes as variants. A row declares what Item works out at runtime (surface, pointer, gap, highlight, ARIA ids), so every use-site compiles to a plain element, per-row state props included.

200 sidebar rows compiled in the app mount in 1.7 ms instead of 38.5 ms with Item and update in 1 ms instead of 22, with the same computed styles at rest, hovered and focused.
The helpers took any value and returned undefined for anything outside their list, so a misspelled token compiled and the style quietly disappeared. Frames call them with literals, so they now accept only the names in fuselage-tokens and always return a value; Box styling props, which receive values at runtime, use lenient resolve* versions that stay internal.
Fuselage frames are all View or Text, so the .is_DOM rules never match an element, yet every style recalc still tries them: about 13% of selector matching on a room page.
…eal item rows

A descendant with :focus-visible is focused, so :focus-within rules out every unfocused row and group header before :has() walks its subtree. The open-menu rule looks only at the row's own actions, which Item and ItemGroupHeader render as direct children.
Port of #43021 to the Tamagui frames: spacing now resolves to px from the token scale, so Box inset props and the message toolbar's offset go through inset(), which keeps its own rem steps.
#42974

Ports #42974 onto the frames, which won the rebase over develop's Box and SCSS versions: Button compact and link types, IconButton badge position, ButtonGroup stretch='equal', Chip small and label, Divider light, flush, small and stretch, Tag gap, Icon colors, Table columns and row headers, Tabs stretch, Tile flush, Skeleton inverted, Modal hidden, ModalBackdrop transparent, ModalTagline subtle, InputBox end-addon-flush, FieldRow justify, and the internal compositions that stopped passing styling props. CardGrid's and Tile's class rules, which develop styles in SCSS, move to plain CSS. Button's block padding follows the size scale, as #43021 did.
…them its variations

Ports #43100 onto the frames, which won the rebase over its Box versions: every component's public props type is now ElementProps plus its declared variations, with className as a string, and the frames gain TableCell truncate, color and strong, Table columns in px and percentages, ModalFooter justify, Modal maxHeight and Tile flush. Styles the PR keeps in SCSS move to plain CSS (NavBar divider, thread follow icons) or useStyle (Callout, StatesIcon, ToastBar, CardGrid's row height).
Since #43100 no caller can pass a styling prop, a free is, an rcx-* prop or a css function as className to a component, so useTamaguiBoxProps had nothing left to translate at render time and is gone. Each component builds its own className and render target, and the styling it used to receive from inside Fuselage (LabelInfo and RequiredMark margins, TableSelection's layout, ToastBar's elevation, Throbber and Icon sizes) lives in its frame. Only Tile, Button, IconButton and Icon, which Margins, Flex and Scrollable still lay out, keep taking BoxTransforms.
Without the Box prop translation, ModalTitle, ModalBackdrop, ModalTagline, ModalFooter, ModalFooterControllers, TableBody, TableFoot, StatesLink, SidebarFooterContent, SidebarButtonGroup, NavBarGroup, MessageBlock and AvatarContainer only style one element, so they are now that frame, and an app that runs the Tamagui compiler can flatten their use-sites. ContextualbarActions, SidebarActions, StatesActions and TableSelectionButtonGroup only renamed ButtonGroup and are now ButtonGroup itself.

A consumer's className now adds to their rcx-* classes (NavBarGroup replaced it, MessageBlock dropped it), and ModalFooter and ModalFooterControllers now forward the props they used to drop.
… names

is-hovered, is-focused, focus-visible and active classes made a frame render every style of that state inline through forceStyle. Only the Storybook state showcases used them, and forceStyle kept those frames from compiling to static classes, so they no longer pin a state. The disabled class still maps to the disabled look.
The proposal's decisions, measurements, migration and SCSS-removal recipes, the cascade layer and provider-free rules, how far the Tamagui compiler takes Fuselage, the plan for removing its runtime from the web build, and the phase-0 backlog of fixes that land on develop first.
… Tamagui components

Ports the base's 6.1 to 6.4 onto the frames and the plain CSS. The style
sheets no longer match the class words `hover`, `active`, `focus`,
`disabled`, `invalid`, their `is-*` forms, `focus.focus-visible` or
`is-visited`; only pseudo-classes, attributes and the components' own
modifiers drive state. The frames stop reading a `disabled` class name
to pick their disabled look or cursor, so `isForcedDisabled` and
`hasDisabledClassName` go away.

The `InputBox` wrapper matches `:has(> .rcx-input-box:invalid)` and
`:has(> .rcx-input-box:disabled)` instead of `invalid` and `disabled`
classes it set from React state, which drops the extra render that state
cost. The selects style invalid and disabled through
`rcx-select--invalid` and `rcx-select--disabled`, and an open one keeps
the focus look through `aria-expanded`.
Text came from develop with an SCSS stylesheet, which the pilot no longer compiles; its scales, colors and truncation become plain CSS imported with the other component stylesheets.
…gui components

The Select family batch moved the default look of the selects, the options list and the mobile dropdown from styling props to SCSS; the pilot had already moved those stylesheets to plain CSS, so the new rules move there: the select value, item, field and chip rows and the trigger layout in Select.styles.css, the list height and surface in Options.styles.css, and the bottom sheet in a new Dropdown.styles.css. The component changes (className and style on the trigger, controlled SelectLegacy, aria-multiselectable, PaginatedMultiSelect ref, derived selection, Dropdown surface attributes, chips removed on click) live in files the pilot leaves alone and need no port.
… components

The contextual bar, ModalContent and ModalIcon stay Box elements, so the stylesheets the batch wrote for them become plain CSS. The Card, CardGrid, CardGroup and TableSelection frames get the batch's semantics: a horizontal Card wraps below the sm breakpoint through a media style instead of useBreakpoints, CardGrid takes element props and sets its row height through a custom property instead of runtime css, CardGroup drops the small and large props, and the selection bar and the empty cell placeholder are styled from the table stylesheet. The pilot already merges className into the card parts, forwards the modal footer props, pads Tile from its frame, and types TableRow without hasAction.
…the Tamagui components

AudioPlayer and Slider stay Box elements, so the stylesheets the batch wrote for them become plain CSS. The frames get the batch's semantics: Bubble and BubbleItem merge a consumer className, the status bullet icons stop emitting an undefined class token, a disabled TabsItem renders a disabled button, FramedIcon honors size, Chip keeps onMouseDown and onClick on their own events, ToastBar renders its id and animates on static keyframes with a --toast-time custom property, the Throbber circles take their timing from custom properties, and AccordionItem collapses through the shared useCollapse hook, so expanded controls it and onToggle reports toggles. Runtime css remains only where the pilot uses it for the ToastBar and Callout icon colors, which the batch did not touch.
…xes to the Tamagui components

The Message, NavBar, Pagination, Sidebar and States frames keep their own rcx class next to a consumer className instead of letting it replace them, and the parts that dropped HTML attributes take them again: the generic preview description, footer and icon, the status indicator text, the thread origin and body, the system block, container and timestamp, the option description block and the message divider. InputBox puts a className only on its wrapper when it has an addon, StatesIcon lets the consumer set its size, SidebarCollapseGroup drops the dead actions prop, SidebarLink is typed as an anchor and SidebarItemAction's onClick as a React mouse event, Divider documents which props apply with children, and Item adds the multiline and interactive modifier classes the pilot's declared lines and interactivity already honor; the Item render-count spec declares interactive, which is how the pilot skips the link's registration. The pilot already keeps Button's rel, logs instead of throwing for Field parts outside Field, and re-exports ButtonGroup and Divider under the removed component names.
…props

Ports #43157 onto the frames: IconButton takes variant and size, which win over the deprecated boolean props, and Banner, SidebarBanner and MessageMetricsFollowing pass size, the latter before the consumer's props so they can override it.
@ggazzo
ggazzo force-pushed the feat/fuselage-tamagui-pilot branch from 39b444c to 58734fd Compare October 27, 2026 02:11
@ggazzo
ggazzo changed the base branch from develop to refactor/fuselage-tamagui-prep October 27, 2026 02:11
The frames keep the 2px and numeric spacing their SCSS used, so they look the same; the spacing lint only checked TypeScript, so those values were never in its baseline. Recording them keeps the ratchet blocking any new off-scale value.

This branch has not been deployed

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

Labels

preview Builds a UI preview that Rocket.Chat Desktop can load

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant