feat(cva): add boolean variant shorthand - #384
Draft
joe-bell wants to merge 2 commits into
Draft
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
cva | 4d78fa2 | Commit Preview URL Branch Preview URL |
Jul 16 2026, 06:15 AM |
joe-bell
force-pushed
the
claude/tailwind-variants-shorthand-75hx0e
branch
from
July 11, 2026 15:14
6625767 to
94ba27e
Compare
cursor
Bot
force-pushed
the
claude/tailwind-variants-shorthand-75hx0e
branch
from
July 12, 2026 10:43
94ba27e to
3ab4281
Compare
Contributor
BenchmarksComparing this PR's local benchmark run against the latest published npm versions. Aim for higher ops/s. Treat deltas within ±5% as noise.
|
| Task | This PR | 0.7.1 |
Δ |
|---|---|---|---|
cva (runtime)component call with defaults |
606,892 ops/s ±0.28% | 618,999 ops/s ±0.27% | -2.0% |
cva (runtime)component call with props |
97,851 ops/s ±0.29% | 98,678 ops/s ±0.27% | -0.8% |
cx (runtime)class join |
5,815,710 ops/s ±0.31% | 6,063,499 ops/s ±0.08% | -4.1% |
cva (static)component definition |
15,442,537 ops/s ±0.08% | 13,999,413 ops/s ±0.32% | 🟢 +10.3% |
compose (static + runtime)define, join and call |
507,856 ops/s ±0.26% | 519,184 ops/s ±0.34% | -2.2% |
cva
| Task | This PR | 1.0.0-beta.7 (beta) |
Δ |
|---|---|---|---|
cva (runtime)component call with defaults |
616,274 ops/s ±0.22% | 618,062 ops/s ±0.29% | -0.3% |
cva (runtime)component call with props |
98,131 ops/s ±0.22% | 96,442 ops/s ±0.26% | +1.8% |
cx (runtime)class join |
5,095,701 ops/s ±0.14% | 4,924,145 ops/s ±0.14% | +3.5% |
cva (static)component definition |
1,572,004 ops/s ±0.11% | 1,889,520 ops/s ±0.32% | 🔴 -16.8% |
composes property (static + runtime)define, join and call |
246,329 ops/s ±0.36% | 251,301 ops/s ±0.22% | -2.0% |
Commit affd871 · Node v24.18.0 · linux x64 · 2026-07-15T13:59:19.126Z
These benchmarks ran in CI on this PR's code. The comment checks that the report looks valid before posting it, but it cannot guarantee the numbers are correct. If you see a large change, re-run the benchmarks locally before treating it as a real improvement or regression.
cursor
Bot
force-pushed
the
claude/tailwind-variants-shorthand-75hx0e
branch
from
July 12, 2026 18:53
3ab4281 to
9005444
Compare
Assigning a class value directly to a variant name is now shorthand
for a boolean variant: `variants: { disabled: "opacity-50" }` expands
to `{ disabled: { true: "opacity-50", false: null } }`, and
`variants: { busy: null }` declares a classless boolean variant that
still participates in props, compoundVariants, and getSchema.
Any non-plain-object value (string, number, array, null) is treated as
shorthand; plain objects always stay variant maps, since a clsx class
dictionary structurally overlaps one (wrap it in an array to use it as
shorthand). Shorthand is normalized once at cva() build time so the
resolution loop, composition merging, component.config, and getSchema
all see the expanded map.
Type-level, the config's variants slot widens to accept shorthand and
VariantProps infers `boolean` for shorthand entries. Two inference
traps surfaced (recorded in AGENTS.md Learnings): the schema value
conditional must be non-distributive or literal defaultVariants values
widen, and local/composed variants must merge via a right-biased
mapped type (MergeVariantShapes) instead of an intersection, which
collapses conflicting option keys to never.
Closes JB-251
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaQeVqbK9krHwPoFPjrP1M
joe-bell
force-pushed
the
claude/tailwind-variants-shorthand-75hx0e
branch
from
July 15, 2026 13:57
9005444 to
c33e972
Compare
Post-review hardening of the boolean variant shorthand: - Rewrite MergedVariants as a right-biased fold over the composed tuple (reusing MergeVariantShapes), mirroring the runtime mergeVariants reduce. The previous UnionToIntersection collapsed conflicting option values across composed components (near-inevitable with shorthand, latent for longhand) to never, poisoning VariantProps and getSchema with string | number | symbol. - Drop bigint from CVAVariantShorthandValue: clsx silently drops bigints, so it typechecked a variant that could never render. - Skip allocation in normalizeVariants when no value is shorthand; document why mergeVariants still copies unconditionally. - Add tests: composed shorthand/longhand conflicts, number and empty-string shorthand, runtime tolerance pins for type-rejected values, explicit-undefined default fallback, compound array selectors, composed getSchema with defaults, bigint rejection. - Bench: feature-detected "Call component (boolean shorthand)" scenario (supportsComposes precedent) so baseline deltas stay meaningful. - Docs: composition and TypeScript pages cover shorthand; the two examples whose disabled maps matched the shorthand expansion byte-for-byte now use it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaQeVqbK9krHwPoFPjrP1M
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a boolean variant shorthand to
cva@beta(tracked in Linear as JB-251): assigning a class value directly to a variant name now declares an on/off variant.variants: { busy: null }declares a classless boolean variant that still participates in props,compoundVariants, andgetSchema.Details:
null) is treated as shorthand. Plain objects always stay variant maps, since a clsx class dictionary structurally overlaps one; wrap a dictionary in an array to use it as shorthand.boolean/undefined/bigintare rejected as shorthand values (bigint because clsx silently drops bigints, so it could never render).cva()build time (with a no-allocation fast path when no shorthand is present), so the resolution loop, composition merging,component.config.variants, andgetSchemaall see the expanded{ true, false }map.getSchemareports shorthand variants as{ values: [true, false] }.VariantPropsinfersbooleanfor shorthand entries, anddefaultVariants/compoundVariantsaccept booleans for them.MergedVariantsused to intersect composed components' variant maps, and TS reduces conflicting option values (null & "x", or two shorthands' differingtrueclasses) tonever, poisoningVariantProps/getSchematostring | number | symbol. It is now a right-biased fold over the composed tuple (reusingMergeVariantShapes), mirroring the runtimemergeVariantsreduce order — option keys still union across components.Call component (boolean shorthand)scenario (same pattern assupportsComposes) keeps baseline deltas meaningful against published versions that lack the shorthand.disabledmaps matched the shorthand expansion byte-for-byte (react-with-tailwindcss,astro-with-tailwindcss) now use it.Three TypeScript inference traps were found and fixed along the way, recorded in
AGENTS.mdLearnings: the schema value conditional must be non-distributive ([T] extends [U]) or literaldefaultVariantsvalues widen; and neither the local-over-composed nor the composed-over-composed variants merge can be a plain intersection (unit-type intersection reduction collapses conflicting option maps tonever).Additional context
pnpm pack+tsc --strict --declarationundernodenext, including a composed-conflict component) confirms no internal types leak into emitted declarations;attw+publintbuild gates pass; all docs// =>output comments executed against the built package; bench smoke run confirms the new scenario registers.MergedVariantsfold ordering (later composed components win per option key, matching the runtime reduce).What is the purpose of this pull request?
Before submitting the PR, please make sure you do the following
fixes #123).🤖 Generated with Claude Code
https://claude.ai/code/session_01VaQeVqbK9krHwPoFPjrP1M