{"id":50422931,"url":"https://github.com/soybeanjs/cva","last_synced_at":"2026-05-31T09:04:12.066Z","repository":{"id":358589997,"uuid":"1241997703","full_name":"soybeanjs/cva","owner":"soybeanjs","description":"Class Variance Authority","archived":false,"fork":false,"pushed_at":"2026-05-18T04:49:02.000Z","size":85,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-18T05:53:38.362Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soybeanjs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-18T03:23:13.000Z","updated_at":"2026-05-18T05:26:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/soybeanjs/cva","commit_stats":null,"previous_names":["soybeanjs/cva"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/soybeanjs/cva","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soybeanjs%2Fcva","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soybeanjs%2Fcva/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soybeanjs%2Fcva/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soybeanjs%2Fcva/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soybeanjs","download_url":"https://codeload.github.com/soybeanjs/cva/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soybeanjs%2Fcva/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33725061,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-31T09:04:11.392Z","updated_at":"2026-05-31T09:04:12.047Z","avatar_url":"https://github.com/soybeanjs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @soybeanjs/cva\n\nHigh-performance Tailwind CSS variant recipes with split `cv` and `scv` APIs.\n\n- `cv`: single-output variant recipes that return one class string\n- `scv`: multi-slot variant recipes that return a slot-to-class map\n- `alias`: remap inherited slot names without changing variant props\n- `derive`: compute variant props from incoming props at call time\n- `defaults`: preset recipe default variants without rebuilding the recipe\n- `extendBase`: compute dynamic base classes or slots from resolved variant props\n- `cn`: flatten `ClassValue` inputs with css-variants style semantics\n- `merge`: merge class parts with `tailwind-merge`\n- `VariantProps`: extract the public variant prop type from a recipe\n- runtime overrides use rest arguments instead of `class` / `className` props\n\n## Installation\n\n```bash\npnpm add @soybeanjs/cva\n```\n\n```ts\nimport { alias, cn, cv, derive, defaults, merge, scv } from '@soybeanjs/cva';\nimport type { VariantProps } from '@soybeanjs/cva';\n```\n\n## Why split `cv` and `scv`\n\nThis package keeps the two common recipe shapes separate:\n\n- use `cv` when the result is one final class string\n- use `scv` when the result is a record of named slots\n\nThat split keeps the runtime small, keeps the types direct, and avoids overloading a single API with two different output models.\n\n## `cn`\n\nUse `cn` when you want css-variants style class flattening for plain `ClassValue` inputs.\n\n```ts\nimport { cn } from '@soybeanjs/cva';\n\ncn('inline-flex', ['items-center', ['justify-center']], { 'font-medium': true, hidden: false });\n// \"inline-flex items-center justify-center font-medium\"\n```\n\n`cn` only flattens values. It does not resolve Tailwind conflicts.\n\n## `merge`\n\nUse `merge` when you already have an ordered list of class parts and want Tailwind conflict resolution.\n\n```ts\nimport { merge } from '@soybeanjs/cva';\n\nmerge(['px-2 text-sm', 'px-4', 'mt-2']);\n// \"text-sm px-4 mt-2\"\n```\n\n`merge` is a thin wrapper around `tailwind-merge` and is useful when classes are already collected as string parts.\n\n## `cv`\n\nUse `cv` for a single class string.\n\n```ts\nimport { cv } from '@soybeanjs/cva';\n\nconst button = cv({\n  base: 'inline-flex items-center rounded-md font-medium',\n  defaultVariants: {\n    size: 'md',\n    tone: 'primary'\n  },\n  variants: {\n    size: {\n      sm: 'h-8 px-3 text-sm',\n      md: 'h-10 px-4 text-sm',\n      lg: 'h-12 px-5 text-base'\n    },\n    tone: {\n      primary: 'bg-blue-600 text-white',\n      secondary: 'bg-slate-100 text-slate-900'\n    },\n    disabled: {\n      false: 'opacity-100',\n      true: 'pointer-events-none opacity-50'\n    }\n  },\n  compoundVariants: [\n    {\n      class: 'shadow-sm',\n      size: 'lg',\n      tone: 'primary'\n    }\n  ]\n});\n\nbutton();\n// \"inline-flex items-center rounded-md font-medium h-10 px-4 text-sm bg-blue-600 text-white opacity-100\"\n\nbutton({ size: 'lg', tone: 'secondary' });\n// \"inline-flex items-center rounded-md font-medium h-12 px-5 text-base bg-slate-100 text-slate-900 opacity-100\"\n```\n\n### `cv` runtime overrides\n\nPass extra classes through rest arguments:\n\n```ts\nbutton({ size: 'lg' }, 'mt-4', ['shadow-lg', 'ring-1']);\n```\n\nOverrides are applied after all base, variant, and compound classes.\n\n### `cv` extension\n\n`cv` can extend other `cv` recipes directly.\n\n```ts\nimport { cv } from '@soybeanjs/cva';\n\nconst surface = cv({\n  variants: {\n    size: {\n      sm: 'text-sm',\n      lg: 'text-lg'\n    },\n    tone: {\n      primary: 'bg-blue-600 text-white',\n      secondary: 'bg-slate-100 text-slate-900'\n    }\n  }\n});\n\nconst button = cv({\n  extend: [surface],\n  variants: {\n    intent: {\n      solid: 'shadow-sm'\n    }\n  }\n});\n```\n\nInherited variant props are part of the child recipe type, and child `defaultVariants` / `compoundVariants` can also target inherited variants.\n\n### `cv.extendBase`\n\nUse `extendBase` when the base classes depend on the fully resolved variant props.\n\n```ts\nimport { cv } from '@soybeanjs/cva';\n\nconst button = cv({\n  base: 'rounded-md',\n  defaultVariants: {\n    size: 'sm',\n    tone: 'primary'\n  },\n  extendBase: props =\u003e [props.tone === 'primary' ? 'ring-1' : 'ring-0', props.size === 'lg' ? 'px-4' : 'px-2'],\n  variants: {\n    size: {\n      sm: 'text-sm',\n      lg: 'text-lg'\n    },\n    tone: {\n      primary: 'bg-blue-500',\n      secondary: 'bg-slate-200'\n    }\n  }\n});\n\nbutton();\n// \"ring-1 px-2 rounded-md text-sm bg-blue-500\"\n\nbutton({ size: 'lg', tone: 'secondary' });\n// \"ring-0 px-4 rounded-md text-lg bg-slate-200\"\n```\n\n`extendBase` runs after inherited `extend` recipes have resolved, and before the local `base` field is appended.\n\n## `scv`\n\nUse `scv` when each slot needs its own final class string.\n\n```ts\nimport { scv } from '@soybeanjs/cva';\n\nconst card = scv({\n  slots: {\n    root: 'rounded-lg border p-4',\n    header: 'mb-2 font-semibold',\n    body: 'text-sm'\n  },\n  defaultVariants: {\n    tone: 'neutral'\n  },\n  variants: {\n    tone: {\n      neutral: {\n        root: 'border-slate-200 bg-white',\n        body: 'text-slate-600'\n      },\n      brand: {\n        root: 'border-blue-200 bg-blue-50',\n        body: 'text-blue-900'\n      }\n    },\n    compact: {\n      false: {},\n      true: {\n        root: 'p-3',\n        header: 'mb-1',\n        body: 'text-xs'\n      }\n    }\n  },\n  compoundVariants: [\n    {\n      class: {\n        root: 'shadow-sm'\n      },\n      compact: false,\n      tone: 'brand'\n    }\n  ]\n});\n\ncard({ tone: 'brand' });\n// {\n//   root: 'rounded-lg border p-4 border-blue-200 bg-blue-50 shadow-sm',\n//   header: 'mb-2 font-semibold',\n//   body: 'text-sm text-blue-900'\n// }\n```\n\n### `scv` runtime overrides\n\n`scv` overrides are also rest arguments, but each argument is a slot map.\n\n```ts\ncard({ tone: 'brand' }, { root: ['mt-4', 'shadow-lg'] }, { body: ['leading-6'] });\n```\n\nEach slot is merged independently.\n\n### `scv.extendBase`\n\nUse `extendBase` when slot base classes depend on the resolved variant props, or when a slot should be filled by another recipe at call time.\n\n```ts\nimport { cv, derive, scv } from '@soybeanjs/cva';\n\nconst button = cv({\n  base: 'inline-flex',\n  defaultVariants: {\n    fitContent: false,\n    size: 'md'\n  },\n  variants: {\n    fitContent: {\n      false: '',\n      true: 'w-fit h-fit'\n    },\n    size: {\n      sm: 'text-xs',\n      md: 'text-sm',\n      lg: 'text-lg'\n    }\n  }\n});\n\nconst iconButton = derive(button, props =\u003e ({\n  fitContent: true,\n  size: props.size === 'lg' ? 'sm' : props.size\n}));\n\nconst card = scv({\n  extendBase: () =\u003e ({\n    close: iconButton()\n  }),\n  slots: {\n    close: '',\n    root: 'rounded-lg'\n  }\n});\n\ncard({ size: 'lg' }).close;\n// \"inline-flex w-fit h-fit text-xs\"\n```\n\nInside `extendBase`, calling another recipe without explicitly passing props reuses the current resolved props. That keeps nested `derive` and `defaults` wrappers composable inside `extendBase`.\n\n## Extending recipes\n\n`scv` can extend:\n\n- another `scv` recipe\n- a slot-mapped `cv` recipe, such as `{ root: someCvRecipe }`\n\n```ts\nimport { cv, scv } from '@soybeanjs/cva';\n\nconst surface = cv({\n  variants: {\n    tone: {\n      neutral: 'bg-white text-slate-900',\n      brand: 'bg-blue-600 text-white'\n    }\n  }\n});\n\nconst panel = scv({\n  extend: [{ root: surface }],\n  slots: {\n    root: 'rounded-xl p-4',\n    title: 'font-semibold'\n  },\n  variants: {\n    tone: {\n      neutral: {},\n      brand: {}\n    },\n    size: {\n      sm: {\n        root: 'p-3',\n        title: 'text-sm'\n      },\n      lg: {\n        root: 'p-6',\n        title: 'text-lg'\n      }\n    }\n  }\n});\n```\n\nDirect `cv` extension is still not allowed in `scv`:\n\n```ts\n// not supported\nscv({\n  extend: [surface]\n});\n```\n\nMap the `cv` recipe to a slot instead:\n\n```ts\nscv({\n  extend: [{ root: surface }]\n});\n```\n\n## Recipe wrappers\n\nUse these helpers when you want to keep recipe metadata intact while changing how variants resolve.\n\n### `derive`\n\n`derive` computes the next variant selection from the incoming props at call time.\n\n```ts\nimport { cv, derive } from '@soybeanjs/cva';\n\nconst button = cv({\n  defaultVariants: {\n    size: 'md'\n  },\n  variants: {\n    fitContent: {\n      false: '',\n      true: 'w-fit h-fit'\n    },\n    size: {\n      sm: 'text-xs',\n      md: 'text-sm',\n      lg: 'text-lg'\n    }\n  }\n});\n\nconst compactButton = derive(button, props =\u003e ({\n  fitContent: true,\n  size: props.size === 'lg' ? 'sm' : props.size\n}));\n\ncompactButton();\n// incoming props are derived before class resolution\n\ncompactButton({ size: 'lg' });\n// resolves as if size were 'sm'\n```\n\nUse this when the next variants depend on the current call's props.\n\nWhen a derived recipe is invoked inside `extendBase`, the outer recipe's current resolved props are used if you do not pass props explicitly.\n\n### `defaults`\n\n`defaults` presets a recipe's `defaultVariants` while keeping explicit call-time props higher priority.\n\n```ts\nimport { cv, defaults } from '@soybeanjs/cva';\n\nconst button = cv({\n  defaultVariants: {\n    fitContent: false,\n    size: 'md'\n  },\n  variants: {\n    fitContent: {\n      false: '',\n      true: 'w-fit h-fit'\n    },\n    size: {\n      sm: 'text-xs',\n      md: 'text-sm'\n    }\n  }\n});\n\nconst iconButton = defaults(button, {\n  fitContent: true,\n  size: 'sm'\n});\n\niconButton();\n// resolves with fitContent=true and size='sm' as defaults\n\niconButton({ size: 'md' });\n// explicit props still override the new defaults\n```\n\nUse this when you want a recipe variant preset, not dynamic remapping.\n\nLike `derive`, a defaulted recipe called inside `extendBase` also inherits the outer recipe's current resolved props when no explicit props are provided.\n\n## `alias`\n\nUse `alias` when you want to inherit an `scv` recipe but expose different slot names in the child recipe.\n\n```ts\nimport { alias, scv } from '@soybeanjs/cva';\n\nconst card = scv({\n  slots: {\n    root: 'rounded-md',\n    body: 'p-4'\n  },\n  variants: {\n    tone: {\n      primary: {\n        root: 'bg-slate-900',\n        body: 'text-white'\n      }\n    }\n  }\n});\n\nconst sectionCard = scv({\n  extend: [alias(card, { root: 'header' })],\n  slots: {\n    header: 'font-semibold'\n  },\n  variants: {\n    tone: {\n      primary: {\n        header: 'uppercase'\n      }\n    }\n  }\n});\n\nsectionCard({ tone: 'primary' });\n// {\n//   body: 'p-4 text-white',\n//   header: 'rounded-md bg-slate-900 font-semibold uppercase'\n// }\n```\n\nAliases also apply to merge input. If a parent slot was renamed from `root` to `header`, runtime overrides should target `header`.\n\n## `VariantProps`\n\nExtract the public variant props directly from a recipe.\n\n```ts\nimport { cv, scv } from '@soybeanjs/cva';\nimport type { VariantProps } from '@soybeanjs/cva';\n\nconst button = cv({\n  variants: {\n    size: {\n      sm: 'text-sm',\n      lg: 'text-lg'\n    }\n  }\n});\n\ntype ButtonProps = VariantProps\u003ctypeof button\u003e;\n// { size?: 'sm' | 'lg' }\n\nconst card = scv({\n  extend: [{ root: button }],\n  variants: {\n    tone: {\n      primary: {\n        root: 'bg-blue-500'\n      }\n    }\n  }\n});\n\ntype CardProps = VariantProps\u003ctypeof card\u003e;\n// { size?: 'sm' | 'lg'; tone?: 'primary' }\n```\n\nInherited variant props from `extend` are included in the extracted type.\n\n## Notes\n\n- `root` has no built-in meaning. It is just a conventional slot name.\n- boolean variants are declared with `'true'` and `'false'` keys and exposed as `boolean` in props.\n- compound variant conditions can use either a single value or an array of values.\n- `extendBase` receives resolved props, which already include inherited and local `defaultVariants` plus the current call's explicit props.\n- unknown props are ignored at runtime.\n- `tailwind-merge` only runs when runtime override arguments are provided. If you do not pass overrides, the recipe returns the prejoined output directly.\n\n## Development\n\n```bash\npnpm test\npnpm typecheck\npnpm build\n```\n\nBenchmark commands are documented in [benchmark/README.md](benchmark/README.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoybeanjs%2Fcva","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoybeanjs%2Fcva","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoybeanjs%2Fcva/lists"}