← Back to overview
Project & Date

Project: Boja  |  Date: July 2026  |  Workstream: 6 of chat-app architecture research

Constraints Recap (Non-Negotiable)

  1. TypeScript everywhere.
  2. React DOM for web (Vite + TanStack Router), Expo/React Native for native.
  3. No shared rendering code — separate component implementations per platform. React Native Web is explicitly rejected.
  4. Shared: design tokens (colors especially), theming system, component semantics/naming/variant vocabulary, business logic.
  5. Full themeability is a first-class product requirement: user-switchable themes, per-server branding, dark/light, semantic color names (primary, secondary, accent, base-100…) — never raw palette values in components.
Design Implication

The design implication of constraints (3)+(4): the shareable artifact is not components but a token contract — a semantic vocabulary of colors/spacing/radii/typography plus variant names (Button variant="primary" size="sm") that both platforms implement independently. Everything below is evaluated against how well it consumes an externally-defined semantic token contract and how well it does runtime multi-theme switching (not just dark/light).


PART A — Web Component/Theming Frameworks

A1. DaisyUI v5 (the founder's favorite)

A2. shadcn/ui + Tailwind v4 tokens

A3. Base UI (v1.0, MUI/ex-Radix team)

A4. Radix Themes (+ Radix Primitives)

A5. HeroUI (formerly NextUI) v2.8+/v3

A6. Mantine (v7/v8)

Also considered, cut: Chakra v3 (fine semantic-token system via Panda-style config, but Emotion-free runtime still its own styling world; smaller momentum than shadcn), Park UI/Ark UI + Panda CSS (excellent Ark primitives + Panda semanticTokens — the most "design-token-native" option — but Panda CSS adds a second build-time styling compiler alongside Tailwind; if the team were not committed to Tailwind, Park UI would make the shortlist).


PART B — Native (React Native/Expo) Styling & Component Systems

Evaluated for: consuming the shared token contract, runtime semantic theme switching, Expo compatibility, maturity.

B1. Unistyles 3 ⭐

B2. NativeWind (v4 → v5)

B3. Tamagui — tokens-only vs full

B4. gluestack-ui v2/v3

B5. React Native Paper

B6. restyle (Shopify)

Runtime semantic theme switching support: Unistyles 3 ✅ (best: no re-render, runtime theme mutation), NativeWind v4/v5 ✅ (CSS vars, subtree-scoped), Tamagui ✅, gluestack ✅ (via NativeWind), Paper ✅ (Context re-render, MD3 vocabulary), restyle ✅ (Context re-render).


PART C — The Token Bridge: @app/design-tokens

C1. Source of Truth: W3C DTCG Token Files

Author tokens once in W3C Design Tokens Community Group format (Format Module reached its first stable version, 2025.10$value/$type/aliases/color/dimension/typography types; reference implementations: Style Dictionary, Terrazzo, Tokens Studio). Two layers:

base-100 / base-200 / base-300 / base-content
primary / primary-content, secondary / secondary-content
accent / accent-content, neutral / neutral-content
info / success / warning / error (+ -content each)
radius-field / radius-box / radius-selector, border, spacing scale

The *-content (foreground-on-color) pairing is the crucial pattern — it's what makes arbitrary user/server themes safe for contrast.

C2. Compiler: Terrazzo

Terrazzo (successor to Cobalt) is DTCG-native, with first-party plugins for CSS variables (selector-scoped theme modes emit [data-theme=x]{...} blocks) and JS/TS (typed token objects + .d.ts). Its lean two-target configuration is the Boja compiler.

C3. Build Outputs

(a) Web — dist/web/themes.css:

@theme inline {           /* Tailwind v4: expose semantic tokens as utilities */
  --color-primary: var(--color-primary);
  --color-base-100: var(--color-base-100); /* … */
}
:root, [data-theme="light"] { --color-primary: oklch(55% .3 250); … }
[data-theme="dark"]  { --color-primary: oklch(65% .25 250); … }
[data-theme="midnight"] { … }

The web path uses shadcn-style owned components on Base UI and generated Tailwind v4 @theme/[data-theme] CSS. DaisyUI's variable vocabulary is the semantic contract.

(b) Native — dist/native/themes.ts:

export const themes = {
  light:   { colors: { base100: '#…', baseContent: '#…', primary: '#…', primaryContent: '#…', … },
             radius: { field: 4, box: 8, selector: 16 }, spacing: { … }, typography: { … } },
  dark:    { … }, midnight: { … },
} as const satisfies Record<ThemeName, AppTheme>;

Colors are compiled from oklch → hex/rgba at build time (RN doesn't parse oklch). Register with Unistyles: StyleSheet.configure({ themes }); the AppTheme type flows into every StyleSheet.create(theme => …) call — full autocomplete on theme.colors.primary.

(c) Shared variant vocabulary: also export from @app/design-tokens (or a sibling @app/ui-contract) the component semantics: type ButtonVariant = 'primary'|'secondary'|'accent'|'ghost'|'outline'; type Size = 'xs'|'sm'|'md'|'lg', plus per-component prop contracts. Web and native each implement <Button variant size> against the same types — this satisfies constraint (4) without sharing rendering.

C4. Runtime Theme Switching on Both Platforms

C5. Monorepo Shape

packages/design-tokens/   tokens/*.json (DTCG) + terrazzo.config → dist/web/themes.css, dist/native/themes.ts, dist/contract.d.ts
packages/domain/          shared pure-TS business logic (decided elsewhere)
apps/web/                 Vite + TanStack Router + Tailwind v4 (+ chosen web kit)
apps/native/              Expo + Unistyles 3 (+ hand-rolled components on @rn-primitives)

Token build runs as a prebuild step (turbo/bun workspace task); CI fails on missing semantic keys per theme (schema check keeps every theme total over the contract).


PART D — Comparison & Recommendation

Web (Part A)

Theming model Multi-theme runtime Breadth Headless/Styled A11y TW v4 Density fit Momentum '25-26
DaisyUI 5 CSS vars, semantic, data-theme, nestable ✅✅ 35 built-in + custom ~60 (CSS-only) Styled, no JS behavior ❌ DIY ✅ native ◐ (styling yes, widgets no) High (solo maintainer)
shadcn/ui CSS vars @theme, semantic ✅ (roll your own scopes) ~50 owned-code Styled over headless ✅✅ ✅ native ✅✅ ✅✅ dominant
Base UI 1.0 none (bring tokens) n/a 35 primitives Headless ✅✅ ✅ (foundation) ✅✅ rising
Radix Themes CSS vars, 12-step scales ~30 + primitives Styled/Headless ✅✅ ◐ not TW-native ▼ waning
HeroUI v3 semantic tokens, React Aria + TW4 ~40 Styled ✅ (v3) ◐ heavy/glossy ◐ rewrite churn
Mantine 8 TS theme → CSS vars ~120 Styled ❌ parallel system

Native (Part B)

Token consumption Runtime semantic switching Components Expo Momentum
Unistyles 3 any TS object — perfect ✅✅ no-rerender, runtime mutation none (BYO) dev build, New Arch ✅✅
NativeWind v4/v5 Tailwind vars — shared vocabulary ✅ CSS vars, subtree scoping via reusables/gluestack ✅ (v5 rollout)
gluestack v2/v3 Tailwind config ✅ (NativeWind) ~40 copy-paste ◐ API churn
Tamagui (tokens-only) createTokens (skip full) ✅ but wrong fit
RN Paper MD3 schema only ✅ (rerender) ~35 Material stable
restyle TS theme — good ✅ (rerender) Box/Text only ▼ low velocity

Final Recommendation

Web Selection

shadcn/ui-style owned components on Base UI primitives + the DaisyUI theme vocabulary as the token contract. Owned components consume the generated semantic utilities and use Base UI for accessible behavior.

Native Selection

Unistyles 3 + hand-rolled component library (mirroring the shared variant vocabulary) on @rn-primitives/react-native-reusables for behavior. It consumes the generated typed theme verbatim, switches themes with zero re-render, and updateTheme makes per-server dynamic branding cheap. NativeWind v5 is the runner-up if the team values one Tailwind mental model over runtime robustness.

Token Pipeline Selection

DTCG (2025.10 stable) token JSON in @app/design-tokens, compiled by Terrazzo → Tailwind v4 @theme + [data-theme] CSS for web and typed themes.ts for Unistyles on native. Theme preference is synced via the user profile; per-server palettes are delivered as semantic token JSON validated for contrast server-side.

This gives the founder DaisyUI's beloved theming semantics everywhere, real accessibility on web, no shared rendering code, and one token source of truth.