Flat Conditional Values

The V3 flat-value syntax and migration workflow

V3 puts every condition in the value of the property it changes. Each property accepts string clauses or a typed object, and both render identically.

Tokens and theme values are bare names. V2 condition objects such as hoverStyle={{ backgroundColor: ... }} have no runtime path.

<html.div className="p-4 sm:p-6 disabled:opacity-50" bg="background hover:background-hover dark:blue-500" />;

Value grammar

Typed objects use modifier names as keys and default for the base value. Compound keys such as 'dark:hover' require every condition to match. These are per-property values, distinct from the removed V2 condition objects.

String clauses use this grammar:

value := base? clause*
clause := modifier (":" modifier)* ":" payload

The base and each payload are ordinary property values. A registered modifier chain starts a new clause; the last matching clause wins.

<html.div className="p-4 sm:p-6 bg-[red] hover:bg-[blue] dark:hover:bg-[navy] w-full md:w-[42rem] shadow-[0_2px_8px_#0003] hover:shadow-[0_4px_16px_#0004] scale-100 enter:scale-[0.9] exit:scale-[0.9]" />;

Payloads run until the next registered modifier chain, including when they contain spaces. An empty payload is invalid; clear a property with a real value such as none, transparent, initial, or unset.

Do not put clauses in the raw transform prop. Use the flattened transform family instead:

<html.div className="scale-100 hover:scale-[1.05] rotate-[0deg] hover:rotate-[5deg]" />;

Token and theme lookup

Quoted identifiers resolve config-first. A configured token, theme key, or variable wins; a miss remains literal text so CSS keywords and custom values pass through.

<html.div className="p-4 border-border-color w-[max-content]" bg="background" />;

Use a qualified token when the same name exists in more than one category:

<html.div className="w-[size.4] color-[color.blue-500]" />;

A raw number remains a platform value:

<html.div p="4" /> // configured space token
<html.div p={4} /> // 4 CSS pixels on web, 4 points on native

Built-in multi-part V3 names are kebab-case. The runtime does not guess aliases:

V2 nameV3 name
backgroundHoverbackground-hover
backgroundPressbackground-press
borderColorborder-color
borderColorHoverborder-color-hover
placeholderColorplaceholder-color
shadowColorshadow-color
V2 name
backgroundHover
V3 name
background-hover
V2 name
backgroundPress
V3 name
background-press
V2 name
borderColor
V3 name
border-color
V2 name
borderColorHover
V3 name
border-color-hover
V2 name
placeholderColor
V3 name
placeholder-color
V2 name
shadowColor
V3 name
shadow-color

Modifiers

The registry combines built-ins with names from your config.

KindSpellings
interactionhover: press: focus: focus-visible: focus-within: disabled:
presenceenter: exit:
themedark: light: and configured theme names
mediaconfigured keys such as sm: md: max-md:
platformweb: native: ios: android: tv: androidtv: tvos:
groupgroup-hover: and named forms such as group-hover/card:
container@sm: and named forms such as @sm/layout:
Kind
interaction
Spellings
hover: press: focus: focus-visible: focus-within: disabled:
Kind
presence
Spellings
enter: exit:
Kind
theme
Spellings
dark: light: and configured theme names
Kind
media
Spellings
configured keys such as sm: md: max-md:
Kind
platform
Spellings
web: native: ios: android: tv: androidtv: tvos:
Kind
group
Spellings
group-hover: and named forms such as group-hover/card:
Kind
container
Spellings
@sm: and named forms such as @sm/layout:

Modifiers in one clause are an AND. For example, dark:hover: applies only while both conditions match.

Merging

Shorthands and longhands merge one property program at a time. A later base replaces the earlier base without erasing unrelated clauses:

<html.div backgroundColor="gray hover:blue" bg="red" />
// backgroundColor is red; hover is blue
<html.div p="4 sm:6" px="2" />
// left and right bases are 2; their sm clauses remain 6

Groups and containers

A group exposes parent state. A query container supplies dimensions.

<html.div group>
<html.span color="gray group-hover:black" />
</html.div>
<html.div container>
<html.span width="100% @sm:50%" />
</html.div>
<html.div group="card" container="card">
<html.span color="gray @sm/card:group-hover/card:black" />
</html.div>

Plain sm: is a viewport query. @sm: measures the nearest query container. Only size-measuring media keys register a container form.

Editor support

For the string spelling, TypeScript can only take autocomplete so far. It offers each prop’s token union plus every bare modifier prefix (hover:, sm:, dark:), but not the cross product: that union is over a million members, which TypeScript rejects outright and then returns no completions at all. So the vocabulary lives in a language server instead.

yarn add --save-dev @tamagui/lsp

It reads .tamagui/tamagui.config.json, the artifact the compiler already writes, and gives you completions filtered as you type, diagnostics for unknown modifiers and misspelled values with “did you mean” suggestions, hover showing a token’s resolved value per theme, and inline color swatches.

Completing a clause replaces only that clause, so accepting background-hover in bg="background hover:back" keeps the background you already wrote.

It speaks stdio LSP, so every editor needs a few lines and no plugin code. The VS Code extension, which spawns the binary for you, is not on the Marketplace yet. For Neovim:

vim.lsp.config.tamagui = { cmd = { 'tamagui-lsp' }, filetypes = { 'typescriptreact', 'javascriptreact' }, root_markers = { 'tamagui.config.ts', 'package.json' }, } vim.lsp.enable('tamagui')

Helix, Zed, Emacs, Sublime and JetBrains are all a similar few lines; see the package README.

If completions never appear, the server is waiting for the config artifact: run your dev server or tamagui generate once so the compiler emits it. When the config changes the server picks it up and republishes on its own, with no editor restart.

For CI and pre-commit, tamagui check validates every flat value in a project and exits non-zero, which is also the right hook for coding agents:

Terminal

tamagui check

Migrate an existing app

The codemod in the Tamagui repository is the migration path. It recognizes old token spellings and V2 condition objects, rewrites every statically eligible site, and reports anything that still needs a human decision.

For example, this V2 source is migration input only:

<View className="p-[$4]" bg="$background" hoverStyle={{ bg: '$backgroundHover' }} $theme-dark={{ bg: '$blue10' }} $sm={{ p: '$6' }} />;

It becomes:

<View className="p-4 sm:p-6" bg="background hover:background-hover dark:blue10" />;

That output preserves the old palette name because the codemod cannot choose a new shade for a custom design system. Check the active config before changing it. blue10 remains valid when the application keeps the frozen v5 pack. If the application moved to v6, replace it with the intended v6 token, for example dark:blue-500, before running the converted application.

Run write mode from your project root against one or more files or directories:

yarn dlx @tamagui/codemod-flat-values --write \ --report flat-values-report.md \ --json flat-values-report.json \ path/to/src path/to/app.tsx

The write is transactional. A source or generated parse error means no files are written. A path that matches no source file exits with status 2, so a typo cannot look like a clean migration.

The report records host capability checks, values that require relocation, and syntax the tool cannot safely infer. Common manual cases include:

  • a condition that changes a component variant rather than a style property;
  • a dynamic condition object or value whose type is not provable;
  • a named container whose declaring parent is outside the scanned file;
  • a transform array or other structured native value;
  • ordering across an opaque spread that may set the same property.

Fix those rows directly in flat syntax. Do not add a compatibility setting or restore a V2 condition-object parser.

After the write, search the migrated source for old authoring:

Terminal

rg -n --glob '*.{ts,tsx,js,jsx}' \ 'hoverStyle|pressStyle|focusStyle|focusVisibleStyle|focusWithinStyle|disabledStyle|enterStyle|exitStyle|\$[A-Za-z][A-Za-z0-9-]*\s*[:=]' \ path/to/src rg -n --glob '*.{ts,tsx,js,jsx}' \ '\$[A-Za-z0-9_.-]+' \ path/to/src

Legacy spellings should remain only in explicit migration fixtures or tooling that reads V2 source. Then typecheck, build, and exercise state, media, theme, group, container, platform, and presence behavior on every target your app ships.

The codemod package itself can be checked with:

yarn test bun run typecheck