UI & Theming Frameworks
DTCG/Terrazzo token pipeline, web and native framework comparisons, runtime theme switching.
Project: Boja | Date: July 2026 | Workstream: 6 of chat-app architecture research
Constraints Recap (Non-Negotiable)
- TypeScript everywhere.
- React DOM for web (Vite + TanStack Router), Expo/React Native for native.
- No shared rendering code — separate component implementations per platform. React Native Web is explicitly rejected.
- Shared: design tokens (colors especially), theming system, component semantics/naming/variant vocabulary, business logic.
- 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.
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)
- Theming model: Best-in-class for this requirement. Pure CSS plugin for Tailwind v4 (
@plugin "daisyui"in CSS — no JS config). Every theme is a set of CSS variables scoped by[data-theme="name"]:--color-base-100/200/300,--color-base-content,--color-primary+--color-primary-content, secondary, accent, neutral, info/success/warning/error, plus non-color tokens (--radius-selector/field/box,--border,--depth,--noise). 35 built-in themes; unlimited custom themes via@plugin "daisyui/theme" { name: "acme"; --color-primary: oklch(...); }. Runtime switching = setdata-themeattribute; themes nest — you can theme a single subtree (<div data-theme="serverBrand">), which maps perfectly onto per-server branding.--default/--prefersdarkflags handle light/dark defaults. - Component breadth: ~60 CSS "components" (btn, chat bubble — literally has a
chatcomponent, menu, dropdown, modal, tabs, badge, avatar, toggle, drawer, …). But they are CSS-only classes: zero JavaScript behavior. No focus trapping, no typeahead listbox, no ARIA wiring beyond what you write. - Headless vs styled: the inverse of headless — styled, behavior-less. You bring the JS (or pair with Base UI/Radix primitives underneath).
- Accessibility: entirely your responsibility for interactive widgets. This is the big caveat.
- Tailwind v4: first-class; DaisyUI 5+ is built for v4 (CSS-file plugin, oklch colors).
- Maturity: very large community (~37k+ GitHub stars, one of the most-installed Tailwind plugins), single primary maintainer (saadeghi) — a bus-factor consideration. Active (v5.6 shipped 2025-2026). MIT.
- Dense chat UI fit: the theming engine is a superb fit; the component look skews rounded/friendly/marketing-ish out of the box, and complex widgets a Discord clone needs (virtualized lists, mention comboboxes, context menus, popover stacking) aren't provided — you'd build them with headless primitives anyway.
A2. shadcn/ui + Tailwind v4 tokens
- Theming model: CSS variables with semantic names (
--background,--foreground,--primary,--accent,--muted,--destructive,--radius…), referenced under Tailwind v4's@theme inlinedirective so every token doubles as a utility class. Multi-theme = multiple[data-theme]/ class scopes redefining the same variables — structurally identical to DaisyUI's mechanism. - Component breadth: ~50 components, copy-paste-owned code (not a dependency). Historically built on Radix primitives; since 2025 the registry is migrating components to Base UI primitives — either way you own the files and can swap internals.
- Headless vs styled: styled recipes over headless primitives; fully editable since the code lives in your repo.
- Accessibility: inherited from Radix/Base UI — excellent (focus management, ARIA, keyboard).
- Tailwind v4: first-class (official Tailwind v4 + React 19 support, codemods,
@themedocs). - Maturity: the dominant React UI approach of 2024-2026 (~90k stars), huge ecosystem (registries, MCP, blocks). MIT.
- Dense chat fit: excellent — neutral, compact, app-like default aesthetic (it's what half the dashboard/app world uses); you own the code so density tweaks are trivial; primitives cover menus/popovers/dialogs/context-menus that a Discord UI needs.
A3. Base UI (v1.0, MUI/ex-Radix team)
- Theming model: none — fully unstyled. You style with whatever (Tailwind + your tokens). Perfect token neutrality.
- Breadth: 35 unstyled components at v1.0 stable (Dec 2025): menu, popover, dialog, combobox/autocomplete, select, toast, tooltip, toolbar, etc.
- Headless: 100% headless, render-prop API, data-attribute state styling (
[data-open]). - Accessibility: its raison d'être; built by the people behind Radix, React Aria adjacency, and MUI, full-time funded team.
- Tailwind v4: trivially compatible (no styles to conflict).
- Maturity: young stable (1.0 Dec 2025) but backed by MUI's full-time engineering; widely viewed as Radix's successor. MIT.
- Dense chat fit: ideal foundation — but it's a primitives layer, not a design system; you pair it with a token/styling layer.
A4. Radix Themes (+ Radix Primitives)
- Theming model:
<Theme>component + CSS variables; 12-step semantic color scales (Radix Colors),accentColor/grayColor/radius/scalingprops; light/dark and nested theme sections supported. Not Tailwind-native. - Breadth: ~30 styled components + the underlying primitives (~28).
- Maturity: large install base, but 2025-2026 momentum concern: WorkOS deprioritized; community reads Base UI as the successor. MIT.
- Dense chat fit: good density (built for apps), but weakest Tailwind v4 story of the six and uncertain trajectory.
A5. HeroUI (formerly NextUI) v2.8+/v3
- Theming model: semantic tokens (primary/secondary/success/warning/danger + layout tokens) defined via plugin; multiple named themes with runtime switching via class on
<html>; v3 rebuilt on React Aria + Tailwind v4 with CSS-variable theming. - Breadth: ~40+ polished styled components.
- Maturity: popular (~23k stars) but went through a disruptive rename (NextUI→HeroUI) and a v2→v3 rewrite spanning 2025-2026; v3 was long in beta. Aesthetic is glossy/animated (Framer Motion dependency) — more consumer-landing than dense app. MIT.
- Dense chat fit: middling — beautiful defaults but heavier DOM/animation weight, less "own your code" flexibility for a Discord-density message list.
A6. Mantine (v7/v8)
- Theming model: TS theme object → CSS variables at runtime;
primaryColor, 10-shade scales,light-dark()support,data-mantine-color-schemeswitching. Powerful, but its own token vocabulary and CSS-modules styling world — not Tailwind-based (coexists, doesn't integrate). - Breadth: the widest of all (~120 components + hooks + form + dates + charts + notifications).
- Tailwind v4: parallel systems; you'd maintain two styling paradigms.
- Maturity: excellent — very active, well-funded via sponsors, v8 (2025) stable. MIT.
- Dense chat fit: capable, but pulls you away from the Tailwind-token pipeline the rest of the stack wants; theme object is Mantine-shaped rather than contract-shaped.
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 ⭐
- Themes are plain TS objects — any shape you want, so the theme type can be generated from the shared token package 1:1 (e.g.
theme.colors.primary,theme.colors.base100). No imposed vocabulary = perfect contract fit. - Runtime switching:
UnistylesRuntime.setTheme('dark')and — killer feature for per-server branding —UnistylesRuntime.updateTheme(name, t => ({...t, colors: {...t.colors, ...serverColors}}))mutates a theme at runtime. C++ core updates styles without re-rendering React components (Fabric/ShadowTree-level updates). Unlimited registered themes; adaptive light/dark mode built in. - Styling only — no components (pair with your own component library + primitives like
@rn-primitivesor hand-rolled). - Requires New Architecture (fine for greenfield Expo SDK 52+); needs dev build (no Expo Go) — acceptable for a real product.
- Very active 2025-2026 (jpudysz; Expo blogged about it), MIT.
B2. NativeWind (v4 → v5)
- Tailwind classNames on RN. Dynamic theming via CSS-style variables (
vars()in v4; v5 has first-class dynamic themes through CSS variables, changeable at runtime, scoped per subtree via a provider). Semantic classes likebg-primaryresolve to variables → same authoring vocabulary as the web app. - Styling only; component layer comes from react-native-reusables (shadcn-for-RN, uses @rn-primitives) or gluestack.
- Tradeoffs: historically buggier releases than Unistyles (v4 had a rocky Expo SDK ride; v5 in rollout during 2025-2026); style resolution happens in JS. But maximal mental-model sharing with the web Tailwind codebase.
- MIT, large community, Expo-endorsed.
B3. Tamagui — tokens-only vs full
- Full Tamagui (compiler +
@tamagui/uicomponents) is optimized for universal apps (RN + RN-Web), which does not fit Boja's separate-renderer architecture; the full stack adds compiler and styled-system complexity for benefits Boja does not use. - Tokens-only (
@tamagui/corecreateTokens/themes) is viable: typed tokens, runtime theme switching, sub-themes. But then you're using ~20% of a heavy framework; Unistyles gives the same with less machinery. - Verdict: skip unless universal rendering ever returns to the table. MIT.
B4. gluestack-ui v2/v3
- Copy-paste RN components (shadcn model) styled with NativeWind/Tailwind; themes via Tailwind config tokens; v3 direction aligns with NativeWind v5 + Tailwind v4. Runtime theme switching inherits NativeWind's variable mechanism.
- Good breadth (~40 components: actionsheet, menu, modal, toast…), decent a11y (RN-aria lineage).
- Maturity caution: the team has rebooted its API surface repeatedly (NativeBase → gluestack v1 styled-system → v2 NativeWind → v3); churn risk. MIT.
- Best role: accelerator on top of NativeWind if that route is chosen — components are copy-pasted so churn is contained.
B5. React Native Paper
- Material Design 3 components; theming = MD3 token schema (
primary,onPrimary,surface…) viaPaperProvider; runtime switching works (swap theme object). But the vocabulary is Material's, not yours; overriding MD3 look for a Discord aesthetic is constant fighting. Mature and stable (Callstack), MIT. - Wrong aesthetic fit — pass.
B6. restyle (Shopify)
- Minimal typed theme-object system (
createTheme,createBox/createText, variants). Theme is a plain TS object → consumes shared tokens perfectly; runtime switching uses a re-renderingThemeProvider(Context-based — full re-render on switch, unlike Unistyles). Solid and stable, but with low development velocity in 2024–2026 and no performance story for frequent theme mutation. - Unistyles remains Boja's native selection.
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:
core.tokens.json— raw palette ramps, spacing scale (4px grid), radii, font sizes/weights, z-index. Themes never touch components directly.themes/<name>.tokens.json— one file per theme (light,dark,midnight,synthwave, …) mapping semantic names to core aliases. Adopt DaisyUI's vocabulary as the contract since the founder loves it and it's proven for multi-theme apps:
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
- Web:
document.documentElement.dataset.theme = name(persist in localStorage + server profile;--prefersdarkhandles system dark). Per-server branding: DaisyUI/CSS-variable themes nest — wrap the server's content region in<div data-theme={serverThemeId}>, or for arbitrary custom brand colors inject a generated[data-theme="srv_<id>"]{…}style tag / inlinestyle={{'--color-primary': …}}overrides. Instant, no re-render beyond style recalc. - Native:
UnistylesRuntime.setTheme(name)for user theme (no React re-render); per-server branding viaUnistylesRuntime.updateTheme('serverOverlay', t => merge(t, serverColors))+setTheme('serverOverlay')when entering a server, or scopedScopedThemecomponent for subtree theming. Server brand colors arrive from the API as semantic-token JSON (same DTCG-shaped payload both clients consume) — validate contrast server-side (APCA/WCAG check on*/*-contentpairs) before accepting per-server palettes. - Sync: theme preference lives in the user profile (synced via Zero like other preferences) → both platforms react to the same source of truth.
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
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.
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.
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.