plans/dom-tailwind-flat-values.md
Design plan, July 2026. This consolidates the current direction for:
@tamagui/tailwind frontend;tamagui/dom entry;The design is intentionally compiler-led. V2 applications receive an explicit V3 codemod, while DOM and Tailwind authoring get strict package and compiler boundaries.
tamagui and @tamagui/core remain regular Tamagui.@tamagui/inline package.@tamagui/tailwind.styleMode.tamagui/dom and
@tamagui/core/dom.style(), not css.create().style(definition) uses the same style-definition grammar as
styled(Component, definition), with the component argument removed.tailwind-merge is removed from Tamagui.$. The V3 migration codemod
removes it from statically known token values. Raw numeric JSX values keep
their existing pixel behavior.bg stops being a fixed alias for backgroundColor and becomes the
complete background candidate family. A resolved color candidate still
writes only backgroundColor; it does not lower blindly to the resetting
CSS background shorthand.$.group-hover: for
an unnamed group and group-hover/card: for a named group.group exposes parent state
and does not establish a size container. On regular Tamagui components,
boolean container is the shorthand for an unnamed
containerType="inline-size" container. Named and full-size containers use
containerName and containerType.sm: are viewport media queries. The
@ prefix is reserved for container queries: @sm: targets the nearest
container and @sm/card: targets a named container.import { View, Text, html, styled } from 'tamagui'
const Card = styled(View, {
p: '4',
bg: 'surface hover:surface-hover',
})
export function Example() {
return (
<html.main p="4">
<html.h1 color="color">Account</html.h1>
<Card>Content</Card>
</html.main>
)
}
The lower-level equivalent remains:
import { View, Text, html, styled } from '@tamagui/core'
Regular Tamagui retains:
styled();View, Text, and component behavior;The V3 style direction changes how conditions are written, while the package and component model remain familiar.
import { View, Text, html, styled } from '@tamagui/tailwind'
export function Example() {
return (
<html.main className="p-4 bg-surface">
<html.h1 className="text-color">Account</html.h1>
<View className="p-4 hover:bg-surface-hover">Content</View>
</html.main>
)
}
@tamagui/tailwind owns:
className authoring;styled() types;It shares the renderer, config, tokens, themes, events, refs, accessibility,
animations, and normalized style output with core. It does not import the
@tamagui/core root or its inline-style frontend.
import { html, style } from 'tamagui/dom'
// lower-level alias: @tamagui/core/dom
const root = style({
display: 'flex',
padding: 16,
backgroundColor: 'surface hover:surface-hover',
})
const heading = style({
color: 'color',
fontSize: 24,
})
export function Example() {
return (
<html.main style={root}>
<html.h1 style={heading}>Account</html.h1>
</html.main>
)
}
style() creates one style handle per call. It does not return a namespace
containing named sub-objects.
Conceptually:
styled(Component, definition)
style(definition)
Both consume the same style-definition grammar. styled() binds the
definition to a component, while style() returns an opaque compiled handle
for the DOM style prop.
The standalone entry contains:
html.*;style();It contains neither regular Tamagui style props nor the Tailwind parser.
The public name is Tamagui DOM or DOM mode. The word strict describes the
contract but is not part of the entrypoint or product name.
regular Tamagui props ──────┐
│
Tailwind class candidates ──┼─> shared property/condition/value IR
│ │
DOM style() definitions ────┘ ├─> web CSS and semantic tags
└─> native style records and primitives
The shared runtime owns:
The frontends own only authoring syntax, types, parsing, and compiler lowering.
Each strict DOM component receives exactly one styling language based on its import source.
| Import | DOM styling language |
|---|---|
tamagui, @tamagui/core | regular Tamagui style props |
@tamagui/tailwind | Tailwind className |
tamagui/dom, @tamagui/core/dom | style() handles |
Examples:
// regular Tamagui
<html.div p="4" bg="surface hover:surface-hover" />
// Tailwind
<html.div className="p-4 bg-surface hover:bg-surface-hover" />
// standalone DOM
<html.div style={root} />
The strict inline DOM types reject Tailwind className. The strict Tailwind
DOM types reject Tamagui style props. Standalone DOM accepts compiled style
handles rather than either frontend.
Ordinary Tamagui components may retain raw className and style where needed
for existing web and React Native interoperability. Core never interprets a
raw className as Tamagui Tailwind syntax.
V3 should align timing-transition authoring with the CSS transition shorthand
and its five longhands:
transitionProperty;transitionDuration;transitionTimingFunction;transitionDelay;transitionBehavior.The leading shorthand direction is:
// CSS defaults fill in property=all, timing=ease, delay=0s, behavior=normal
transition="200ms"
// complete CSS shorthand, including per-property transitions
transition="opacity 150ms ease-out, transform 250ms cubic-bezier(0.2, 0, 0, 1) 50ms"
The expanded form uses the real web longhands rather than a second Tamagui-specific object:
transitionProperty="opacity, transform"
transitionDuration="150ms, 250ms"
transitionTimingFunction="ease-out, cubic-bezier(0.2, 0, 0, 1)"
transitionDelay="0ms, 50ms"
transitionBehavior="normal"
Configured animation-driver presets remain useful for springs and shared product motion:
transition="quick"
transition="bouncy"
The parser needs one deterministic resolution rule. The proposed rule is that
an exact single identifier matching a configured animation key is a preset.
Every other string follows the CSS transition grammar. CSS global values and
reserved transition keywords cannot be animation preset names. Duration-shaped
values such as 200ms always use CSS semantics, even if a legacy config has a
same-named preset. The V3 codemod must rename or expand a colliding preset when
its configured easing differs from the CSS default.
Both forms lower into one transition IR containing property, duration, timing function, delay, and behavior. The CSS driver serializes that IR without changing supported web semantics. Native timing drivers translate supported durations and easing functions. Spring presets remain driver configuration rather than pretending to be CSS.
Native does not silently approximate unsupported web behavior. Examples that
need capability diagnostics include unsupported properties, steps(), and
allow-discrete. The native capability table and the migration of the existing
array and per-property preset object forms require a focused prototype before
the V3 contract is locked.
All conditional styling belongs to the individual property value. Top-level style definitions stay flat.
The new form replaces:
hoverStyle, pressStyle, focusStyle, and other pseudo-style objects;$theme-* sub-objects;$platform-* sub-objects;The V3 grammar uses Tailwind candidate suffixes inside each style prop. The prop supplies the utility family:
<View
p="4 sm:6"
bg="red-500 hover:blue-500 dark:hover:blue-700"
w="full md:[42rem]"
/>
Conceptually:
bg="red-500 hover:blue-500"
-> bg-red-500 hover:bg-blue-500
The implementation does not need to construct class-name strings. It binds the
candidate family before sending each suffix through the same candidate parser
and ordered style IR used by @tamagui/tailwind.
Group state uses Tailwind's modifier grammar unchanged. A group exposes parent state and does not make the parent a size container:
// unnamed group
<View group>
<Text color="muted group-hover:foreground" />
</View>
// named group
<View group="card">
<Text color="muted group-hover/card:foreground" />
</View>
The equivalent Tailwind frontend marks the named parent with group/card and
uses the same group-hover/card: modifier on descendants. Tamagui-specific
native states extend the same shape, for example group-press/card:. Variants
remain stackable, such as sm:dark:group-hover/card:foreground.
Viewport and container conditions have different spellings:
// viewport sm plus card hover
color="muted sm:group-hover/card:foreground"
// nearest container sm plus parent hover
color="muted @sm:group-hover:foreground"
// named layout container sm plus named card group hover
color="muted @sm/layout:group-hover/card:foreground"
The @ prefix does not mean media in general. Tailwind uses ordinary sm:,
md:, and related modifiers for viewport media. Only container query variants
use @sm:, @md:, @max-md:, and their named forms.
The parent capabilities are explicit and can be combined on one element:
// regular Tamagui
<View group="card" container />
// Tailwind Tamagui
<View className="group/card @container" />
On regular Tamagui base components, container is boolean-only:
// nearest unnamed inline-size container
<View container />
// named inline-size container
<View container containerName="layout" />
// named container that queries both axes
<View containerName="layout" containerType="size" />
container lowers to containerType="inline-size" and does not create a
container name. An explicit containerType replaces the shorthand and cannot
be supplied together with container. containerType="size" corresponds to
Tailwind's @container-size; adding containerName="layout" corresponds to
the /layout modifier.
The boolean shorthand belongs only to regular Tamagui components. Tailwind
Tamagui uses @container, and standalone style() retains the actual CSS
container, containerName, and containerType properties. Native treats the
regular props as semantic query-container configuration.
This changes current Tamagui behavior, where group="card" also emits
container-name: card and defaults container-type to inline-size. V3
removes that coupling so hover-only groups do not pay container setup or native
measurement costs.
Tailwind arbitrary values are the literal escape hatch:
bg="[linear-gradient(135deg,_#f00,_#00f)]"
w="[117px] md:[344px]"
bg="(--my-background)"
Using this exact spelling lets regular and Tailwind Tamagui share one parser. The brackets also give whitespace, functions, slashes, and modifier boundaries one unambiguous representation.
Raw JSX numbers remain raw platform values, including the existing numeric pixel behavior:
p={16}
w={117}
A numeric string is a candidate, so p="4" resolves the configured 4 token.
Conditional raw values use brackets, for example p="4 hover:[18px]".
The Tailwind-derived V6 palette already uses kebab-case names such as
slate-500. The remaining inherited semantic theme and token names should
follow the same convention:
backgroundHover -> background-hover
backgroundPress -> background-press
borderColorHover -> border-color-hover
placeholderColor -> placeholder-color
This makes the same configured value read consistently in both frontends:
// regular Tamagui
bg="background hover:background-hover"
// Tailwind Tamagui
className="bg-background hover:bg-background-hover"
Underlying config storage may still use $ until the token representation is
migrated, but $ is absent from the new flat candidate syntax. The V3 codemod
converts built-in camelCase names to their V6 kebab-case replacements.
User-defined names retain their authored spelling, with kebab-case recommended
for new configuration. The parser must not guess camelCase-to-kebab-case
aliases at runtime because two configured names could collide.
bg is a candidate family, not a direct alias for the CSS background
shorthand. The candidate resolver determines whether a value contributes
backgroundColor, backgroundImage, backgroundPosition, or another
background property. This avoids the reset behavior of emitting
background: ... for every ordinary color.
Overloaded Tailwind families require property validation. For example,
fontSize="xl" and color="red-500" both bind to the text-* family, then
verify that the resolved candidate contributes to the property named by the
prop. A mismatched candidate is a compiler diagnostic.
The selected syntax is exact scoped Tailwind:
bg="red-500 hover:blue-500"
bg="[linear-gradient(135deg,_#f00,_#00f)]"
This removes $, uses one parser, and gives arbitrary values one explicit
form. Keeping $ in candidate atoms and interpreting unknown function-shaped
tokens as bare CSS were considered and rejected. Both would create a second
permanent value path.
The candidate grammar must not become an exhaustive TypeScript template literal union. Property types retain their existing raw value type plus a broad string:
type FlatStyleValue<T> = T | (string & {})
Candidate, token, modifier, arbitrary-value, and target validation live in the compiler and a Tamagui language server backed by the same candidate engine. Thin editor integrations launch that server through the Language Server Protocol. TypeScript can expose small finite unions where they remain cheap, but type performance takes priority over exhaustive string validation.
The earlier condition-call proposal was:
bg="$red :hover($green) :light($color1/50) :dark:hover($color2/20)"
Compact input may be accepted:
bg="$red:hover($green):dark:hover($blue)"
Documentation and formatting always use one space before each conditional clause:
bg="$red :hover($green) :dark:hover($blue)"
This is a property-centered conditional program. It reaches a similar intermediate representation to StyleX and React Strict DOM conditional value objects, with a surface that composes naturally through JSX props.
The rest of this section records that earlier proposal so its cascade, condition, native, and validation requirements are not lost. The chosen public grammar is property-scoped Tailwind candidates, and no condition-call implementation path remains.
program = base-value? clause*
clause = condition+ "(" property-value ")"
condition = ":" registered-condition
Example parse:
[
{ when: [], value: '$red' },
{ when: ['hover'], value: '$green' },
{ when: ['light'], value: '$color1/50' },
{ when: ['dark', 'hover'], value: '$color2/20' },
]
A program may omit its base:
bg=":hover($green)"
Empty clauses are invalid:
// compile error
bg="$red :hover()"
Property values with defined semantics handle clearing. Examples include
none, transparent, initial, or unset where supported. Empty
parentheses do not mean deletion.
The parser cannot split on colons, whitespace, commas, slashes, or parentheses. All appear in valid CSS values:
background="linear-gradient(to right, red 0%, blue 100%)"
shadow="0 2px 8px rgb(0 0 0 / 20%) :hover(0 4px 16px rgb(0 0 0 / 30%))"
backgroundImage="url(https://example.com/a:b)"
width="calc(100% - var(--sidebar-width))"
fontFamily="'IBM Plex Sans', sans-serif"
Parsing follows CSS component-value rules:
:<registered-condition>( before interpreting text as a clause.The compiler and runtime parser share one implementation and one test corpus.
Conditions form one registry assembled from built-ins and Tamagui config.
Initial categories:
hover, press, active, focus, focusVisible,
focusWithin, disabled, checked, selected, open, invalid;light, dark, and configured theme conditions;web, native, ios, and android;Simple conditions use bare registered names:
opacity="1 :disabled(0.5)"
display="flex :sm(grid)"
bg="$surface :dark($surfaceDark)"
Conditions compose as an AND chain:
bg="$surface :dark:hover($surfaceDarkHover)"
Parameterized group or container conditions need one canonical form. The condition-call proposal used:
bg="$surface :group[card]:hover($surfaceHover)"
That spelling does not carry into the property-scoped candidate direction.
The selected candidate spelling is group-hover/card:surface-hover, matching
Tailwind's named-group grammar. Container queries use @sm: or the named
@sm/card: form and stack independently with group state.
There is one global condition namespace. Duplicate configured names are reported when the config is created. The implementation must not silently choose between a theme, media, platform, or state condition with the same name.
Exact theme matching semantics must reuse Tamagui's established theme inheritance rules. The flat syntax must not invent a second definition of whether a parent theme condition matches a child theme.
Within one property program, clauses are evaluated left to right. The last matching clause wins:
bg="$red :hover($green) :dark($gray) :dark:hover($blue)"
When dark mode and hover are active, $blue wins.
A later property contribution replaces the entire earlier property program. It does not recursively merge clauses:
const Card = styled(View, {
bg: '$surface :hover($surfaceHover)',
variants: {
danger: {
true: {
bg: '$red',
},
},
},
})
The danger variant removes the earlier hover behavior. Preserving a
conditional behavior requires restating the complete property program:
bg="$red :hover($redHover)"
This atomic property replacement is intentional. It makes component props, variants, spreads, and overrides predictable without recursive object merging.
Ordinary independent atomic classes cannot represent authored clause order. The order of class names in HTML does not control the CSS cascade, and selector specificity can override source order.
The compiler therefore lowers a complete property program together. One possible output is a hash shared by identical property programs:
._bg_ab12 {
background: var(--red);
}
._bg_ab12:where(:hover) {
background: var(--green);
}
:where(.dark) ._bg_ab12 {
background: var(--gray);
}
:where(.dark) ._bg_ab12:where(:hover) {
background: var(--blue);
}
Requirements:
The exact CSS encoding may differ, but these semantics are fixed.
Tamagui tokens keep $:
bg="$green"
Real CSS custom properties retain CSS syntax:
bg="var(--green)"
Bare --green is not the token syntax. In CSS, --green names a custom
property declaration and is not itself a value. Keeping $ makes the
cross-platform Tamagui token distinct from a web-only CSS variable.
Color-token opacity uses:
bg="$green/50"
Initial rule:
Unconditional values keep their natural representation:
opacity={0.5}
zIndex={2}
Conditional values use a string program:
opacity="0.5 :hover(1)"
zIndex="2 :focus(3)"
The v4 direction prefers CSS-shaped strings for structured styles:
transform="scale(1) :hover(scale(1.05))"
boxShadow="0 2px 8px #0003 :hover(0 4px 16px #0004)"
React Native object and array values that lack a faithful CSS-shaped replacement require an explicit migration table. Legacy structured values must not be converted by guesswork.
Literal programs compile completely:
bg="$surface :hover($surfaceHover)"
Dynamic programs require the shared runtime parser unless the surrounding entrypoint is compile-only:
bg={`${base} :hover(${hover})`}
Runtime parsing is cached by property, program, and config. Dynamic values receive the same validation and injection protections as existing dynamic styles.
Standalone DOM is compile-only and reports dynamic structures that cannot be lowered safely. Existing regular Tamagui components remain runtime-correct during the v3 migration. Whether ordinary v4 components require compilation is a separate release-level decision.
The full grammar must not be modeled as a recursive TypeScript template literal union. That would recreate the styled type-performance problem.
The public property type remains broad:
type ConditionalStyleValue<T> = T | (string & {})
Validation belongs in:
TypeScript may provide lightweight token or condition suggestions, but type performance takes priority over exhaustive validation.
Property-scoped candidates and Tailwind classes lower into the same ordered IR:
// regular Tamagui
bg="red hover:green dark:hover:blue"
// Tailwind Tamagui
className="bg-red hover:bg-green dark:hover:bg-blue"
Both become:
[
{ property: 'backgroundColor', when: [], value: '$red' },
{ property: 'backgroundColor', when: ['hover'], value: '$green' },
{ property: 'backgroundColor', when: ['dark', 'hover'], value: '$blue' },
]
Shared internals include:
The two public syntaxes do not import one another.
tailwind-merge is removed completely.
For Tamagui-owned candidates:
<View className="p-2 p-4" />
The Tailwind frontend emits ordered contributions. Tamagui's resolver makes the later padding contribution win.
Unknown classes are preserved for official Tailwind on web:
<View className="grid-cols-2 grid-cols-3" />
Tamagui does not attempt to merge candidates it does not own. Applications
that dynamically compose unknown Tailwind utilities can install and call
tailwind-merge themselves. It is never a dependency of core or
@tamagui/tailwind.
@tamagui/tailwind/vite owns:
Wrapping official React Strict DOM would combine two component and style runtimes.
Official RSD currently owns:
Tamagui already owns equivalents for much of this. A wrapper would also have to reconcile:
style contract;className;Official RSD remains useful as:
It is not a production dependency.
The first complete target is the same 49 html.* elements exposed by RSD,
with explicit element-specific prop interfaces.
The strict contract targets:
data-*;RSD deliberately excludes capture-phase JSX event props. DOM mode follows that strict contract. Capture listeners require the underlying event target API where the target supports it.
Ordinary Tamagui components may expose a broader web-only prop surface. Those
props carry clear @platform web JSDoc and do not weaken strict DOM mode.
The initial web-only prop gap found in regular Tamagui included:
onAuxClick;onFocusIn and onFocusOut;onFullscreenChange and onFullscreenError;onGotPointerCapture and onLostPointerCapture;onPointerEnter and onPointerLeave;onPointerOut and onPointerOver;onKeyDownCapture and
onWheelCapture.The regular component expansion should use explicit generated or curated
interfaces. Intersecting every Tamagui component with React's complete
HTMLAttributes is rejected until a type-performance benchmark proves it
safe.
The July 2026 comparison found:
| Surface | Concrete style keys |
|---|---|
| Official RSD | 148 |
| Tamagui View on web | 244 |
| Tamagui Text on web | 263 |
| Tamagui View on native | 187 |
| Tamagui Text on native | 202 |
At that snapshot, Tamagui covered 139 of RSD's 148 style names on web and 131 on native. Some apparent gaps were API-shape differences:
transition;pointerEvents as a style while Tamagui exposes it as a prop.The flat conditional-value syntax brings Tamagui closer to RSD's property-centered model without adopting StyleX handles as the regular Tamagui authoring syntax.
Native cannot promise every CSS behavior. Known difficult areas include:
calc() and clamp() where React Native cannot evaluate them;Unsupported behavior gets a compile diagnostic. DOM mode does not silently invent an approximation.
Tags are classified at compile time as View-backed, Text-backed, interactive, media, input, or unsupported.
The compiler:
html.tag bindings using module provenance.jsx/jsxs, and createElement forms through the
shared element IR.The runtime does not scan resolved children to rescue dynamic strings. Dynamic children retain normal React Native constraints and errors.
Native primitives own only behavior that cannot be erased:
DOM-shaped refs and events exceed React Native host capabilities in places. The contract is the proven subset, documented per tag and platform.
V1 does not promise:
HTMLElement implementation;Compatibility expands only through concrete fixtures and conformance tests.
Import provenance selects the frontend:
| Binding source | Compiler interpretation |
|---|---|
tamagui, @tamagui/core | DOM plus regular Tamagui props |
@tamagui/tailwind | DOM plus Tailwind candidates |
tamagui/dom, @tamagui/core/dom | DOM plus style() handles |
All lower to:
typed html element
→ validate tag, props, styles, and nesting
→ classify native backing primitive
→ normalize ordered property programs
→ emit target output
Example:
<html.main p="$4">Hello</html.main>
Approximate web output:
<main className="_p4">Hello</main>
Approximate native output:
<DOMView style={styles.p4}>
<DOMText>Hello</DOMText>
</DOMView>
Rules:
html.* usage requires the DOM compiler;View and Text retain their established runtime path.@tamagui/tailwind is also compiler-led. It does not ship the Tailwind parser
inside every rendered component. Dynamic class values require a statically
bounded compiler representation or a documented Tailwind-package runtime
cost. Core never pays that cost.
Conceptual exports:
{
"tamagui": {
".": "regular Tamagui plus html",
"./dom": "standalone DOM plus style()"
},
"@tamagui/core": {
".": "regular Tamagui plus html",
"./dom": "standalone DOM plus style()",
"./internal-runtime": "private shared implementation boundary"
},
"@tamagui/tailwind": {
".": "Tailwind Tamagui plus html",
"./vite": "Tailwind build integration"
}
}
@tamagui/core/internal-runtime is implementation plumbing, not another user
package. Its exact name is not public API unless package tooling requires an
export.
Barrels must not reconnect the frontend graphs. Runtime exports and type exports require separate bundle and declaration checks.
Core keeps its regular component prop graph and removes global styling-mode branches.
Flat conditional values add a broad string alternative to each style property. They do not create recursive condition object types or exhaustive template literal unions.
Tailwind components expose normal component behavior plus:
type TailwindStyleProps = {
className?: string
}
Their style types do not instantiate the regular Tamagui shorthand, pseudo, media, theme, and variant prop graph.
type DOMProps = StrictHTMLProps & {
style?: CompiledStyle | readonly ConditionalCompiledStyle[]
}
CSS property checking happens at the style() call. It is not intersected
into every JSX tag.
Generate explicit interfaces from a checked-in semantic table. The generated surface must:
data-* support;HTMLAttributes into every component.Type performance is an acceptance criterion, not a post-implementation cleanup.
The July 2026 probes used tiny minified production bundles with React and
React Native externalized. Browser export conditions were used for web, and
the module graph was checked for .native files.
Corrected View results:
| Configuration | Web gzip |
|---|---|
main @tamagui/web | 24.3 KB |
| v3-beta as shipped | 43.9 KB |
v3-beta with tailwind-merge externalized | 35.6 KB |
v3-beta with tailwind-merge and style grammar externalized | 31.5 KB |
The v3-beta browser bundle contained zero .native modules. It did contain
the Tailwind pipeline even when Tailwind mode was disabled at runtime.
Measured contributors:
tailwind-merge: about 8.3 KB gzip;getSplitStyles;The expected v3-beta result after fully removing Tailwind from core was approximately 25 to 30 KB gzip.
The same probe, with StyleX externalized, measured official RSD html.div at
about 3.4 KB gzip on web and 15.8 KB on native. Those numbers describe RSD
glue rather than the complete StyleX requirement.
Current styledHtml added about 1 KB gzip to an existing Tamagui graph.
Layering official RSD on Tamagui added roughly 3.3 KB on web and 15.4 KB on
native in that probe, while also duplicating runtime responsibilities.
These measurements are comparison evidence, not permanent budgets. The final implementation gets fresh repeatable probes.
Required gates:
View contains zero Tailwind grammar, parser, merger, or build modules;View contains zero regular inline-style frontend modules;.native modules enter a browser-conditioned probe;Initial targets:
Package imports define the mode:
import { View as InlineView } from '@tamagui/core'
import { View as TailwindView } from '@tamagui/tailwind'
Both use the same provider and config:
const config = createTamagui({
tokens,
themes,
media,
})
Migration:
@tamagui/tailwind.@tamagui/tailwind.There is no global switch and no combined mode.
V3 makes the flat candidate grammar canonical. A component does not carry two competing condition systems.
Examples:
// existing
<View
bg="$surface"
hoverStyle={{ bg: '$surfaceHover' }}
$theme-dark={{ bg: '$surfaceDark' }}
/>
// flat values
<View
bg="surface hover:surface-hover dark:surface-dark"
/>
Group migration distinguishes parent state, viewport media, and container queries:
// existing: group hover only
<View group="card">
<Text $group-card-hover={{ color: '$foreground' }} />
</View>
// V3
<View group="card">
<Text color="group-hover/card:foreground" />
</View>
// existing: nearest group container size plus hover
<View group>
<Text $group-sm-hover={{ color: '$foreground' }} />
</View>
// V3
<View group container>
<Text color="@sm:group-hover:foreground" />
</View>
The codemod adds the boolean container prop only when descendants use a group
size condition. Pure $group-hover and $group-card-hover cases remain
groups without containers. Existing viewport media combined with group state
becomes sm:group-hover/card:, without @.
Named legacy group-container queries preserve their name during migration:
$group-card-sm-hover becomes
@sm/card:group-hover/card:foreground, and the parent receives
group="card" container containerName="card". New code should normally use
the nearest unnamed container and reserve the repeated named form for cases
that require disambiguation.
The codemod can convert statically local cases. It must report cases where:
V3 removes the old condition object path on a release schedule with a codemod and diagnostics.
In v3, bg changes from an alias for backgroundColor to the complete
background candidate family. Ordinary color uses remain mechanically
migratable:
// current
bg="$surface"
// exact scoped-candidate direction
bg="surface"
The compiler resolves each candidate to its actual background contribution. It
must not emit the CSS background shorthand for every value because that would
reset separately authored background image, position, size, repeat, attachment,
origin, and clip values.
The migration tool can remove $ from statically known token values, convert
conditional objects into modifiers, and wrap raw literals in Tailwind arbitrary
value brackets. It reports dynamic strings and spreads whose meaning cannot be
resolved locally.
Existing View and Text code does not need to migrate.
DOM adoption is explicit:
import { html } from 'tamagui'
Libraries that publish html.* source either publish compiled output or
declare that consumers must compile the dependency.
styleMode from public types and runtime branches.getSplitStyles to regular Tamagui responsibilities.@tamagui/tailwind.tailwind-merge.@tamagui/tailwind/vite.html.div.jsx/jsxs, and createElement normalization.html from core.html from @tamagui/tailwind.tamagui/dom and @tamagui/core/dom.style() on the same style grammar as styled().$, camelCase V6 built-ins, and conditional
objects.html objects;One shared source fixture covers:
Compare supported behavior with a pinned official RSD fixture. Differences are recorded in the compatibility table.
data-* coverage;style();Repeat the established minified gzip probes for:
View;View;html.div;html.div;html.div plus one style() handle;Record module graphs with every number.
Native RSD and DOM-polyfill work is sensitive to small amounts of per-element overhead. React Strict DOM PR #512 reports roughly a five percent improvement in its benchmark from changes that appear locally small:
Final passes over Tamagui DOM and RSD-aligned code must treat every hook, callback ref, wrapper object, style array, default-prop object, context read, and tag-specific polyfill on the generic native host path as a measured cost. Optional behavior must not allocate or subscribe when the corresponding prop or feature is absent.
The native benchmark gate covers:
html.div, html.span, View, and Text
trees;Record the benchmark command, device or runtime, sample size, variance, and before/after numbers. Bundle size alone cannot approve a native DOM-path change.
@tamagui/inline;tailwind-merge dependency;$token and --token syntax;strict-dom product or entrypoint name.These decisions need focused prototypes before implementation is considered locked:
style() conditionally composes multiple handles while preserving
whole-property replacement.Each prototype must end with one chosen path. The implementation must not ship multiple equivalent syntaxes or runtime fallback paths.
styledHtml introduction: commit ab8517c5e4