* fix: read variant tokens from flattened class selectors
Tailwind 4.3.3 stopped nesting variants under the utility class and emits
compound selectors instead:
<= 4.3.2 4.3.3
.active\:x { &:active { ... } } .active\:x:active { ... }
The native processor only read `:active`, `:focus`, `:disabled`, theme
`:where(...)`, `:dir()` and `[data-*]` tokens from nested rules. On the
flattened form the leading class token was accepted and the rest of the
selector ignored, so every variant compiled as an unconditional style:
`active:bg-red-500` was red at rest, `disabled:opacity-50` always dim,
`dark:` always on.
Read variant tokens from the components that follow the class token too,
sharing one reader with the nested path. A flattened compound the runtime
cannot observe (`disabled:` also emits `[aria-disabled="true"]`) is
dropped, matching what its empty nested rule compiled to before.
Regression test feeds both selector shapes straight to ProcessorBuilder,
so it does not depend on which Tailwind the repo pins. With
`@tailwindcss/node` at 4.3.3 the existing suite goes from 11 failures
(pressable, touchables, data-attributes, dir) to green.
* chore: bump tailwind dependencies to 4.3.3
Root catalog, @tailwindcss/node and @tailwindcss/oxide in the package, and
@tailwindcss/vite in the vite example. The repo now runs its own suite
against the flattened variant selectors that 4.3.3 emits.
* fix: handle flattened theme roots on web and skip unobservable compounds
Two follow-ups from review and CI on Tailwind 4.3.3.
Web: Tailwind now flattens `:root { &:where(.dark, .dark *) {} }` into a
sibling `:root:where(.dark, .dark *) {}` rule. The rule visitor only
rewrote nested theme variants into `.dark`, so the flattened form kept its
`:root` prefix and a scoped `.dark` class could not select it; the
`bg-background in dark theme` e2e test failed. Route a theme-layer `:root`
rule whose second token is `:where(.theme)` through the same rewrite.
Native: a selector mixing a supported variant with a token the runtime
cannot observe (`disabled:active:` emits `[aria-disabled="true"]:active`)
used to keep the branch gated on `:active` alone. Track unsupported tokens
in `readSelectorVariants` and skip the whole selector instead of applying
it under a weaker condition.
14 KiB
Uniwind Context
This document captures working context for packages/uniwind, the published uniwind package in this monorepo. Keep it current when architecture, public APIs, supported platforms, or build/runtime contracts change.
Product
Uniwind is Tailwind CSS bindings for React Native and React Native Web. It lets users write className props on React Native components while doing as much style work as possible at build time.
Primary promise: fast Tailwind styling for React Native with web parity where practical.
Positioning: Uniwind optimizes for build-time Tailwind-to-React-Native artifacts, minimal runtime work, and React Native Web parity. It is not primarily a full design-system/runtime styling framework, a NativeWind compatibility layer, or a web-first CSS bridge.
Core user-facing features:
- Out-of-the-box
classNamebindings for React Native components. - Tailwind v4 CSS compilation into native runtime style artifacts or web CSS.
- Light, dark, and extra named themes.
active,focus,disabled, RTL, orientation, responsive, data attribute, and platform-aware variants.- CSS custom property reads and updates from React Native code.
- Scoped themes through
ScopedTheme. - Scoped layout direction through
LayoutDirection. - Scoped CSS variables through
ScopedVariables. - Metro and Vite integration.
Supported platforms: iOS, Android, web, Android TV, and Apple TV. Other React Native targets are out of scope until tests and docs explicitly cover them.
Package Boundaries
Important paths:
packages/uniwind/src/index.ts: public package entrypoint.packages/uniwind/src/components: React Native component wrappers and web exports.packages/uniwind/src/core: runtime config, listeners, native style store, and web style extraction.packages/uniwind/src/hooks: public hooks.packages/uniwind/src/hoc:withUniwindfor custom components.packages/uniwind/src/bundler: Metro/Vite adapters, Tailwind compilation, CSS processing, artifact generation.packages/uniwind/tests: native, web, type, and e2e tests.packages/uniwind/uniwind.css: package-level CSS artifact referenced by packagestyleexport.
Public exports from src/index.ts:
Uniwindruntime/config object.LayoutDirectioncomponent.ScopedThemecomponent.ScopedVariablescomponent.withUniwindHOC and related types.useCSSVariable,useResolveClassNames,useUniwindhooks.ThemeNameandUniwindConfigtypes.
Package subpath exports:
uniwind: main runtime API.uniwind/components: React Native component replacements.uniwind/components/*: individual component replacements.uniwind/metro: Metro adapter.uniwind/vite: Vite plugin.uniwind/types: generated/user-facing type support.
Stability policy: public package and subpath exports are semver-stable. Generated artifact internals are implementation details unless explicitly documented, with two notable user-facing surfaces: generated theme typings and the package style export (uniwind.css).
Dependency policy: peer dependency floors are support contracts. Raising support floors for Tailwind, React, or React Native requires semver-major unless an upstream ecosystem break makes that impossible to honor.
Runtime Model
Native runtime:
- Build output injects a generated stylesheet callback into
Uniwind.__reinit(...). UniwindStoreholds generated style records, theme variables, scoped variables, runtime state, and per-theme caches.UniwindStore.getStyles(className, props, state, context)resolves classes into React Native style objects.- Cache keys include class names, component state, whether theme is scoped, layout direction, and a key derived from the merged
ScopedVariablesmap. - During resolve,
ScopedVariablesoverrides are overlaid onto a prototype-chained clone of the theme vars so unset variables fall through to the theme. - Resolved styles subscribe to only dependencies they use, then invalidate cache entries on change.
- Runtime dependencies are represented by
StyleDependency: theme, dimensions, orientation, insets, font scale, RTL, adaptive themes, and variables. - Native style resolution filters rules by screen width, orientation, theme, RTL, active/focus/disabled state, and
data-*props. - Native post-processing adapts CSS concepts to RN shapes, including line-height multipliers, shadows, transforms, gradients, visibility, borders, outlines, font variants, and filters.
Web runtime:
- Web keeps styles in CSS and passes
{ $$css: true, tailwind: className }through RNW style arrays. getWebStylesuses a hidden DOM element to compute style values when a JS value is needed, such as color extraction oruseResolveClassNames.CSSListenertracks active CSS rules and media queries, then notifies subscribers when class-dependent media rules change.ScopedThemerenders adivwith the theme class anddisplay: contentson web.LayoutDirectionrenders a contents-style wrapper withdirection/dirsemantics so RTL/LTR variants can be scoped to a subtree.ScopedVariablesrenders adisplay: contentswrapper and sets its variables as inline custom properties on that wrapper, so the real DOM cascade resolvesvar(--name)to the scoped value for every descendant (numbers become px). During JS reads (getWebVariable/useResolveClassNames) it also applies the variables to the hiddendummyParent, then clears them.- Dynamic CSS variable updates are written into a generated
#uniwind-dynamic-stylesstyle element.
Shared runtime:
Uniwind.setTheme(theme | 'system')switches explicit themes or returns to system-adaptive light/dark.Uniwind.currentThemeandUniwind.hasAdaptiveThemesbackuseUniwind.Uniwind.updateCSSVariables(theme, variables)updates theme variables and notifies variable subscribers.Uniwind.updateInsets(insets)is native-only behavior and updates safe-area-style runtime values.ScopedThemesetsUniwindContext.scopedTheme; scoped subtree ignores global theme changes for style resolution.LayoutDirectionsetsUniwindContext.rtl; scoped subtree uses that direction for RTL/LTR variant resolution instead of global runtime RTL.ScopedVariablessetsUniwindContext.variables; the subtree overrides CSS variables for style resolution anduseCSSVariablewithout mutating the global theme. Nested providers merge with ancestors, nearest wins.
Build And Bundler Model
Configuration shape:
cssEntryFile: required CSS entry path, resolved fromprocess.cwd().extraThemes: optional named themes added to defaultlightanddark.dtsFile: optional generated declaration file path, defaultuniwind-types.d.ts.- Metro-only
polyfills.rem: custom rem base, default16. - Metro-only
debugandisTVflags exist in types.
Compilation flow:
compileTailwindreadscssEntryFile, runs Tailwind v4 compile, scans files under the CSS entry directory, and builds final CSS.compileCSSroutes to web or native by platform.compileWebCSSruns Lightning CSS withUniwindCSSVisitorand returns CSS.compileNativeCSSrunsProcessorBuilder, serializes variables, scoped variables, and native stylesheet metadata into JS source.UniwindBundlerConfig.generateArtifactswrites CSS artifacts and generated theme typings.- Internal package aliases such as
@/*are only safe insidepackages/uniwind/src/bundler. Bundler files are built and transformed to JS, but runtime/component/hook/HOC files are published directly as.ts/.tsxReact Native entrypoints, so aliases in those files are not rewritten.
Metro integration:
withUniwindConfig(config, uniwindConfig)patches Metro graph support for uncached modules.- Metro adds
cssas source extension and removes it from asset extensions. - Metro transformer handles the configured CSS entry file specially.
- Metro transformer worker selection is lazy, cached per Expo/non-Expo config type, and follows Expo transformer paths or Expo-specific config markers.
- Native platform CSS transforms into a JS module that calls
Uniwind.__reinit(...)with a fingerprint of the generated styles and themes. During development, the native runtime skips reinitialization when that fingerprint is unchanged. - Web platform CSS transforms into CSS plus web runtime setup.
- Resolver swaps React Native component imports to Uniwind-aware implementations where needed.
Vite integration:
uniwind(config)returns a pre-Vite plugin.- Vite aliases
react-nativeto Uniwind web components, except imports from Uniwind internals resolve back toreact-native-web. - Vite replaces RNW
createOrderedCSSStyleSheetwith Uniwind's ordered stylesheet implementation. - Vite uses Lightning CSS with
UniwindCSSVisitor. - Vite generates artifacts on
buildStartandgenerateBundle.
CSS Processing
Native processing converts Tailwind-generated CSS into metadata-rich style records.
Important concepts:
- A
Stylerecord stores entries, breakpoint bounds, orientation, theme, RTL, native flag, dependencies, source index, class name, important properties, selector complexity, pseudo-states, and data attributes. - CSS variables live in
vars; theme and platform-scoped variables live inscopedVarswith internal prefixes. - The processor treats declarations under
:rootor outside class rules as variables. - Theme variants are recognized from known theme names.
- Variant tokens (
:active,:focus,:disabled,:where(.theme),:dir(),[data-x]) are read from two selector shapes: nested under the class as&:active(Tailwind < 4.3.3) and flattened into the class selector as.active\:x:active(Tailwind >= 4.3.3). A selector carrying any token the runtime cannot observe (e.g.[aria-disabled="true"], alone or stacked with a supported variant) is skipped, never applied under a weaker condition. - Data attribute variants support boolean
data-xand exactdata-x="value"matching against component props. - Media queries drive dimensions, orientation, color scheme, platform, and native/web-specific metadata.
- Important declarations are preserved as
importantProperties. - Unsupported CSS features may be silently ignored on native. Prefer documenting support coverage over adding noisy runtime failures for every unsupported CSS construct.
- Tailwind composes
filterfrom per-utility--tw-*variables and relies onvar(--x,)empty fallbacks for unset parts, soVarresolves those to an empty string. Each filter function compiles tort.filterFn(name, amount, unit)becauseaddMissingSpaceswould otherwise corrupt an inlineblur(${...}px)template. - Filter runtime support is platform-dependent: Android applies filters at the default release level (blur and drop-shadow need API 31+, and one blur in the chain sends the whole chain down that path), while iOS renders blur/grayscale/saturate/contrast/hue-rotate only behind the
enableSwiftUIBasedFiltersReact Native feature flag — experimental in RN 0.83-0.86, canary in 0.87, absent before 0.83. backdrop-filterhas no RN equivalent and is still dropped.
Web visitor behavior:
- Theme root rules in Tailwind theme layer become theme class rules, whether the variant is nested under
:root(Tailwind < 4.3.3) or flattened into:root:where(.dark, .dark *)(Tailwind >= 4.3.3). - Theme-prefixed class rules are scoped with CSS
@scopeto selected theme classes and excluded from other themes. - Visitor state is cleaned between transforms.
Components And HOC
Native components:
- Native wrappers import the underlying
react-nativecomponent. useStyle(className, props, state)resolvesclassNamethroughUniwindStoreand subscribes to style dependencies.- Most components combine generated style before user style:
[generatedStyle, props.style], preserving user overrides. - Stateful components such as
Pressablepasspressed,focused, anddisabledstate into style resolution. - Accent-capable components use
accentColorextraction helpers where needed.
Web components:
- Web wrappers import from
react-nativeas resolved by bundler aliases. - Web wrappers map
classNameto RNW CSS style markers throughtoRNWClassName. - Web wrappers pass generated
dataSetso data attribute variants can match.
withUniwind:
- Auto mode maps
className-style props to matching RN style props and color class props to color props. - Manual mode maps custom class props to custom target props and can extract a single style property.
- Native mode resolves to concrete RN style objects.
- Web mode usually emits RNW CSS style markers and uses computed style only for extracted values.
Testing And Quality Gates
Package scripts:
bun run build: unbuild package outputs.bun run check:typescript: TypeScript no-emit check.bun run lint: oxlint onsrc.bun run circular:check: dpdm circular dependency check.bun run test:native: Jest native tests.bun run test:web: Vitest web tests.bun run test:types: type-level tests.bun run test:e2e: Playwright e2e tests.
Root scripts use Turbo for monorepo-wide build, typecheck, lint, test, format, and circular checks.
Testing layout:
tests/native: component behavior and native style parsing.tests/web: web config, components, and HOC behavior.tests/type-test: public type expectations.tests/e2e: browser checks for web style extraction and generated artifacts.
Source-of-truth policy: repository code and tests win for implementation details. External docs at docs.uniwind.dev describe intended public behavior and should be updated when public behavior changes.
Engineering Constraints
- Runtime performance matters. Prefer build-time CSS processing and narrow runtime invalidation over broad recomputation.
- Preserve user style precedence when adding component wrappers.
- Keep native and web behavior aligned unless platform constraints require divergence.
- Any new runtime dependency should map to
StyleDependencyand invalidate only affected subscribers. - Theme-aware changes must account for global theme, adaptive system theme, and
ScopedTheme. - CSS variables must keep lazy getter semantics on native because values may depend on current runtime state.
- Avoid introducing compatibility paths without known consumers or persisted behavior.
- Add tests for native, web, and types when changing public API or cross-platform behavior.