Skip to content

StyleX

Hozo can also read a static same-file StyleX sheet into the same IR. This works alongside Tailwind on one element and lowers through the ordinary Web and Native backends:

import * as stylex from '@stylexjs/stylex'
import { View } from '@hozo/core'
const styles = stylex.create({
root: { padding: 16, backgroundColor: '#2563eb' },
active: { opacity: 0.7 },
})
export function Card({ active }: { active: boolean }) {
return <View className="rounded-xl" {...stylex.props(styles.root, active && styles.active)} />
}

The transform order is load-bearing: Hozo must run before the official StyleX transform. Hozo consumes stylex.props from the original TSX; the StyleX pass afterward eliminates the now-unused stylex.create definition. Running StyleX first turns the spread into a second className, after which JSX last-wins semantics have already lost the information needed to combine the two frontends safely. Vite plugin order should therefore put hozo() before the StyleX plugin; Metro’s Hozo wrapper already sends its rewritten source to the configured Babel transformer afterward.

The first slice accepts a namespace import, a same-file module-scope static stylex.create, static string/number values, and stylex.props(styles.base, condition && styles.variant). It covers the common universal layout, spacing, size, colour, opacity, radius and text properties, including border/outline, text-decoration, blend, pointer and sizing keywords. Against StyleX 0.19.0’s published types that is 505/522 property names (96.7%), including 134/134 (100%) when the denominator includes both React Native’s published keys and exact compile-time Native equivalents, and 21/21 (100%) contextual-runtime names. Web-only lowering is reported independently at 349/366 (95.4%). The reviewed compiler-relevant surface is 504/504 (100%). Including optional platform adapters it is 505/505 (100%): backdropFilter uses an opt-in Native component adapter. The other 17 published names are not current compiler work: StyleX itself deliberately disallows 13 shorthands in its default mode, two are @font-face descriptors, and two are obsolete or non-standard. These are property-name upper bounds: each property still accepts only the statically safe value subset the shared IR can represent.

The 522-name denominator and every mapped/unmapped claim live in the generated packages/tailwind-conformance/stylex-manifest.json. Each entry records its Universal, Contextual, Adapter, or Web-only lane and the implementation basis for counting it. Conformance reports consume that manifest rather than parsing Rust source formatting; a drift test regenerates it from pinned StyleX and React Native types plus the frontend lowering table. After a deliberate frontend or dependency change, refresh it with pnpm --filter @hozo/tailwind-conformance stylex:manifest.

Property names are no longer the only StyleX score. The executable practical corpus currently measures 349/349 (100%) representative values, 19/19 (100%) common authoring constructs, and 271/271 (100%) declarations after weighting the same values across Card, Typography, Input, Scroll, Motion, Grid, and Border scenarios. Every representative value runs the Hozo Web and Native compilers and counts only when Web agrees with the official StyleX Babel output and Native either lowers faithfully or follows the entry’s explicit Web-only policy. Unsupported cases may remain official residuals, but do not earn coverage. The current corpus has zero silent failures.

Generated content supports the exact common static subset: normal, none, quoted strings, and quote-control keywords. Function forms such as attr() and counter() remain with the official StyleX compiler until their wider grammar and pseudo-element use are represented without accepting arbitrary CSS text. The caret shorthand expands into independently ranked caret-color and caret-shape slots on Web, so either longhand can override it with StyleX’s property-specificity ordering. Native refuses the shorthand explicitly because React Native cannot preserve its shape component.

Static StyleX textShadow values are portable when they are none or one explicitly coloured layer whose offsets and optional blur use px/zero. Web keeps the authored CSS semantics; Native expands the declaration to textShadowColor, textShadowOffset, and textShadowRadius. Multi-layer, relative-unit, and dynamic values remain with the official StyleX transform. Portable placeContent similarly expands common flex alignment values into independent alignContent and justifyContent slots on both platforms.

The common single-family font shorthand lowers exactly on Web for optional normal/italic style and numeric or keyword weight, a static size, optional line-height, and one identifier or quoted family. Keeping the shorthand intact preserves its full reset behavior. Variant/stretch syntax, family lists, system fonts, and dynamic values remain official residuals with explicit Native refusal.

StyleX’s standalone translate, rotate, and scale properties use typed IR instead of becoming Web-only CSS strings. Web preserves their authored component count and agrees with the official atomic CSS; Native composes them into its transform array in CSS’s fixed translate-rotate-scale order. The portable subset covers one- or two-axis px/% translation, degree rotation, and one- to three-axis numeric/percentage scale. Wider syntax remains residual.

Common animation control longhands are exact Web-only declarations: composition, signed delay, direction, fill mode, non-negative iteration count, play state, and keyword, cubic-bezier(), or steps() timing functions. Native consumes them with an explicit Web-only diagnostic rather than implying that arbitrary CSS keyframes run on React Native. A module-scope static stylex.keyframes referenced by animationName is carried in typed IR, content-hashed, hoisted once on Web, and follows the same explicit Native refusal policy; exported sheets carry it through the project module registry. Static keyframe references also preserve the official fallback order in firstThatWorks(...) and value arrays. Dynamic keyframes, simultaneous animation-name lists, and wider easing syntax remain with the official StyleX compiler. Practical single animationRange, scrollTimeline, and viewTimeline shorthands expand into independently ranked longhand slots on Web; lists and dynamic timeline functions remain residual.

Common Web compositing and 3D controls lower without runtime CSS parsing: clipPath, perspective, perspectiveOrigin, transformBox, transformStyle, and willChange. Their accepted static grammar is validated and Native refusal remains explicit. A single static non-negative backdropFilter: 'blur(Npx)' on an ordinary View lowers through an opt-in Native component adapter; none is a no-op. Expo integration is supplied as a factory without making expo-blur a Hozo dependency. CSS blur radius and Expo intensity are different scales, so their mapping is explicitly configurable. Filter chains, conditional blur, and incompatible component contexts remain residual or diagnosed.

The common mask longhands are exact Web-only declarations: prefixed and standard mask images, mode, repeat, position, size, origin, clip, composite, and type. The accepted subset includes ordinary URLs and gradients, layered keyword values, and static length/percentage positions and sizes. The practical single-layer mask shorthand remains a CSS shorthand so its mask-border resets are preserved, while its image, position, size, repeat, box, mode, and composite components are validated before lowering. The source-first maskBorder shorthand likewise reuses the validated slice, width, outset, repeat, and mode grammar while retaining its CSS resets. Multiple layers, wider image functions, variables, and less common component orderings remain with the official StyleX compiler instead of being approximated.

Modern anchor placement covers StyleX’s published positionArea keywords and the common static positionTry, positionTryFallbacks, and legacy positionTryOptions fallback lists. Named fallbacks and the three flip tactics are validated and emitted exactly on Web; variables and wider future grammar remain official residuals, with explicit Native refusal.

CSS corner shapes cover the shorthand, four logical corners, and four physical corners using StyleX’s six published keywords: round, scoop, bevel, notch, square, and squircle. Open-ended strings such as future superellipse() syntax remain official residuals until their grammar is stable enough to validate; Native refusal is explicit.

Static motion paths and float shapes cover the common offsetAnchor, offsetDistance, offsetPath, offsetPosition, and offsetRotate longhands, plus physical float/clear, shapeOutside, shapeMargin, and shapeImageThreshold. Safe paths, rays, basic shapes, positions, angles, and lengths lower exactly on Web. The common single-path offset shorthand also expands to its five independently ranked longhand slots, including CSS initial values. Position/anchor slash forms, direction-relative float values, calculations, and newer shape syntax remain official residuals.

Common border-image longhands cover borderImageSource, borderImageSlice, borderImageWidth, borderImageOutset, and borderImageRepeat. Ordinary URLs and gradients, fill, static number/length/percentage lists, and repeat keywords lower exactly on Web. The practical source-first borderImage shorthand is also validated and kept as a CSS shorthand so its resets remain exact. Wider image functions, calculations, variables, and less common component orderings remain with the official StyleX compiler; Native refusal remains explicit.

Implicit browser Grid now covers gridAutoColumns, gridAutoRows, gridAutoFlow, and gridTemplateAreas. Static track sizes, dense flow, and rectangular named-area templates lower exactly on Web and fail explicitly on Native. Calculated or repeated implicit tracks, variables, and non-rectangular area definitions remain with the official StyleX compiler. The practical track-only grid: "rows / columns" shorthand reuses the same contextual Native track runtime. On Web it additionally emits the shorthand’s required initial resets for template areas, implicit tracks, and auto-flow; area-bearing and auto-flow forms remain residual.

The static construct slice now flattens recursive stylex.props arrays, preserves logical and ternary guards, and expands module-local const object literals used only through object spreads. A mutable, exported, late-declared, escaped, or dynamically produced spread stays with the official StyleX pass. Static stylex.firstThatWorks(...) candidates for one typed property also lower without losing the fallback: Web emits the official reverse declaration order, while Native selects the first candidate its style model can represent. Dynamic, empty, multi-declaration, or mismatched candidates remain residual.

The first explicit Web-only slice covers closed-keyword appearance, color-scheme, image-rendering, overflow/overscroll, print-color adjustment, resize, scroll snap, scrollbar width/gutter, text rendering, and touch-action longhands. Their CSS is differential-tested against the official StyleX plugin. Native consumes the StyleX spread but emits WEB_ONLY_PROPERTY_ON_NATIVE rather than silently dropping the declaration; values outside the checked keyword grammar remain with official StyleX. The second Web-only slice reuses existing typed IR for integer order, axis-specific overflow, scroll behavior, physical/logical scroll margin and padding longhands, and text indentation. This keeps their established Tailwind lowering and Native refusal policy rather than introducing another CSS-string path. The common columns, columnRule, and listStyle shorthands expand to their final Web-only slots before atomic priority is resolved. Physical scrollMargin and scrollPadding expand their exact one-to-four-value box syntax; their Block and Inline counterparts expand one or two values along the logical axis. Ambiguous or wider values remain residual.

The layout shorthand slice expands flexFlow, gridGap, gridRowGap, and gridColumnGap into the same typed properties React Native already carries, so the aliases work on both platforms without runtime code. container expands into the existing contextual name/type IR. Their shorthand/longhand priority is resolved per final property slot, including conditional rules.

The border-axis slice covers the width, style, and color longhands that the official StyleX compiler actually emits. Logical width and color aliases lower to existing Native edge properties. Side-specific styles remain exact on Web and fail explicitly on Native, whose single borderStyle cannot preserve different styles per edge. The compound border/borderTop family remains unmapped because StyleX 0.19 itself rejects those shorthands in its default property-specificity mode.

The contextual slice is currently Grid: static gridTemplateColumns/ gridTemplateRows tracks made from positive fr, non-negative px, or minmax(px, fr) values (plus equal-track repeat), and integer line or auto/equal-span/full-span item placement. A four-line numeric gridArea shorthand expands to row/column start/end and shares the same Native path. A track-only gridTemplate similarly splits rows from columns. On Native these reuse HozoGrid and HozoGridItem; unsupported CSS Grid values remain with StyleX and produce STYLEX_NOT_LOWERED instead of being approximated. Static transition property, duration, delay, and timing configuration also reaches the existing Native interaction/ambient transition runtime. The accepted subset is deliberately limited to properties and easing curves that runtime can interpolate faithfully. The practical single-transition shorthand expands to all four reset slots on both platforms, and Native passes its delay through to Animated.timing; lists, negative delays, and wider easing syntax remain with official StyleX. Static container, containerName, and containerType also reuse HozoContainer on Native. The supported type subset is normal, size, and inline-size, and the runtime currently accepts one conservative CSS identifier as its lookup name. Multiple names and wider CSS syntax remain with official StyleX. StyleX 0.19’s default property-specificity mode is preserved for the supported static slice. Hozo resolves the same four atomic priority tiers before entering the shared IR, so physical longhands still beat later shorthands on both Web and Native; conditional arguments retain their fallback and override behavior. This is frontend-only work and adds no runtime priority machinery.

A mixed rule no longer makes its supported declarations all-or-nothing. Hozo lowers the supported static properties and rebuilds the unsupported properties as a residual inline stylex.create; the official transform continues to own that residual, while its generated className is combined with Hozo’s class instead of replacing it. Conditional props() arguments keep the same guards. If the residual overlaps a lowered property family, or the rule contains an opaque object spread/computed key, Hozo keeps the original call intact rather than approximating the cascade.

Static sheets may live in another file and pass through named, star, or namespace re-export chains. Vite, Next, and Metro feed their authoritative alias resolution into the shared module registry; Metro keeps those answers separate per platform. Unsupported members remain with official StyleX.

createTheme, keyframes, unsupported nested selectors/at-rules, and sx remain with StyleX. Direction-dependent logical/physical edge conflicts and Grid shorthand/line conflicts also remain explicit STYLEX_NOT_LOWERED gaps: their Native result needs runtime context or a more expressive Grid IR, so Hozo does not approximate them.