{"id":"ptekspy/cva","name":"cva","scope":"ptekspy","platform":"roblox","description":"Class Variance Authority for Roblox React - compose component props with variants","version":"1.1.0","latest":"1.1.0","versions":["1.1.0"],"license":"Apache-2.0","licenseRating":"safe","licenseCaveats":["Modified files must carry a notice of changes. If the package ships a NOTICE file, its attributions must be preserved."],"licenseVerified":true,"dependencies":{},"integrity":"4c4b800f9a3f3b1950840e299d50dbc0336875406669b00a60892a84d4f5d258","likes":0,"downloads":0,"install":"forest install ptekspy/cva","url":"https://forest.dev/p/roblox/ptekspy/cva","files":"https://api.forest.dev/ai/package/roblox/ptekspy/cva/files","readme":"# CVA\r\n\r\n**C**lass **V**ariance **A**uthority for Roblox React.\r\n\r\nA powerful utility library for composing Roblox GUI element properties (props) with type-safe variants, default variants, and compound variants. CVA helps you build reusable, maintainable component style systems that scale across your project.\r\n\r\nInspired by [class-variance-authority](https://cva.style/), but adapted for React Lua component development in Roblox.\r\n\r\n## Features\r\n\r\n- **Type-safe variants** — Define component styles with named variants and variant options\r\n- **Default variants** — Set sensible defaults that can be overridden\r\n- **Compound variants** — Combine multiple variant conditions to apply specific props\r\n- **Props merging** — Intelligent merging ensures base props, variants, compounds, and overrides are applied in the correct order\r\n- **Clean API** — Simple, declarative API inspired by industry-standard CVA\r\n- **React Lua integration** — Works seamlessly with React Lua components\r\n\r\n## Installation\r\n\r\n```bash\r\nwally add ptekspy/cva\r\n```\r\n\r\n## Usage\r\n\r\n### Basic Example\r\n\r\nDefine button styles with intent (primary/secondary) and size (sm/md/lg) variants:\r\n\r\n```lua\r\nlocal React = require(game:GetService(\"ReplicatedStorage\").Packages.React)\r\nlocal cva = require(game:GetService(\"ReplicatedStorage\").Packages.CVA)\r\n\r\nlocal buttonStyles = cva({\r\n\tSize = UDim2.fromOffset(180, 44),\r\n\tBorderSizePixel = 0,\r\n\tAutoButtonColor = true,\r\n\tFont = Enum.Font.GothamBold,\r\n}, {\r\n\tvariants = {\r\n\t\tintent = {\r\n\t\t\tprimary = {\r\n\t\t\t\tBackgroundColor3 = Color3.fromRGB(255, 170, 0),\r\n\t\t\t\tTextColor3 = Color3.fromRGB(20, 20, 20),\r\n\t\t\t},\r\n\t\t\tsecondary = {\r\n\t\t\t\tBackgroundColor3 = Color3.fromRGB(40, 40, 40),\r\n\t\t\t\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\t\t\t},\r\n\t\t},\r\n\t\tsize = {\r\n\t\t\tsm = {\r\n\t\t\t\tSize = UDim2.fromOffset(120, 36),\r\n\t\t\t\tTextSize = 16,\r\n\t\t\t},\r\n\t\t\tmd = {\r\n\t\t\t\tSize = UDim2.fromOffset(180, 44),\r\n\t\t\t\tTextSize = 20,\r\n\t\t\t},\r\n\t\t\tlg = {\r\n\t\t\t\tSize = UDim2.fromOffset(240, 56),\r\n\t\t\t\tTextSize = 26,\r\n\t\t\t},\r\n\t\t},\r\n\t},\r\n\tdefaultVariants = {\r\n\t\tintent = \"primary\",\r\n\t\tsize = \"md\",\r\n\t},\r\n})\r\n```\r\n\r\n### Using in a React Component\r\n\r\nUse the returned function in your component to merge variant styles with component props:\r\n\r\n```lua\r\nlocal function Button(props)\r\n\treturn React.createElement(\"TextButton\", buttonStyles({\r\n\t\tintent = props.intent,\r\n\t\tsize = props.size,\r\n\t\tText = props.Text,\r\n\t\t[React.Event.Activated] = props.onActivated,\r\n\t}))\r\nend\r\n\r\n-- Usage\r\nReact.createElement(Button, {\r\n\tintent = \"primary\",\r\n\tsize = \"lg\",\r\n\tText = \"Click Me\",\r\n\tonActivated = function()\r\n\t\tprint(\"Button clicked!\")\r\n\tend,\r\n})\r\n```\r\n\r\n### Overriding Default Variants\r\n\r\nYou can override default variants on a per-call basis:\r\n\r\n```lua\r\n-- Uses default variants: intent=\"primary\", size=\"md\"\r\nlocal primaryButton = buttonStyles({ Text = \"Save\" })\r\n\r\n-- Override the intent variant\r\nlocal secondaryButton = buttonStyles({\r\n\tintent = \"secondary\",\r\n\tText = \"Cancel\",\r\n})\r\n\r\n-- Override both variants\r\nlocal smallPrimaryButton = buttonStyles({\r\n\tsize = \"sm\",\r\n\tText = \"Delete\",\r\n})\r\n```\r\n\r\n### Direct Prop Overrides\r\n\r\nYou can always override any prop directly, even those set by variants:\r\n\r\n```lua\r\nbuttonStyles({\r\n\tintent = \"primary\",\r\n\tText = \"Click Me\",\r\n\tBackgroundColor3 = Color3.fromRGB(0, 0, 0), -- Overrides intent's color\r\n\tTextSize = 14, -- Overrides size's TextSize\r\n})\r\n```\r\n\r\n### Compound Variants\r\n\r\nCompound variants let you apply specific props when a combination of variants matches. This is useful for special styling when certain variant combinations occur:\r\n\r\n```lua\r\nlocal buttonStyles = cva(\r\n\t{ BorderSizePixel = 0 },\r\n\t{\r\n\t\tvariants = {\r\n\t\t\tintent = {\r\n\t\t\t\tprimary = { BackgroundColor3 = Color3.fromRGB(255, 170, 0) },\r\n\t\t\t\tdanger = { BackgroundColor3 = Color3.fromRGB(220, 60, 60) },\r\n\t\t\t},\r\n\t\t\tsize = {\r\n\t\t\t\tsm = { Size = UDim2.fromOffset(120, 36) },\r\n\t\t\t\tlg = { Size = UDim2.fromOffset(240, 56) },\r\n\t\t\t},\r\n\t\t},\r\n\t\tdefaultVariants = {\r\n\t\t\tintent = \"primary\",\r\n\t\t\tsize = \"sm\",\r\n\t\t},\r\n\t\tcompoundVariants = {\r\n\t\t\t{\r\n\t\t\t\tintent = \"primary\",\r\n\t\t\t\tsize = \"lg\",\r\n\t\t\t\tprops = {\r\n\t\t\t\t\tTextSize = 30,\r\n\t\t\t\t\tBackgroundColor3 = Color3.fromRGB(255, 200, 0), -- Brighter gold for large primary\r\n\t\t\t\t},\r\n\t\t\t},\r\n\t\t\t{\r\n\t\t\t\tintent = \"danger\",\r\n\t\t\t\tsize = \"lg\",\r\n\t\t\t\tprops = {\r\n\t\t\t\t\tTextSize = 28,\r\n\t\t\t\t\tBackgroundColor3 = Color3.fromRGB(255, 80, 80), -- Brighter red for large danger\r\n\t\t\t\t},\r\n\t\t\t},\r\n\t\t},\r\n\t}\r\n)\r\n\r\n-- When intent=\"primary\" AND size=\"lg\", the compound variant props are applied\r\nlocal largeButton = buttonStyles({ intent = \"primary\", size = \"lg\" })\r\n```\r\n\r\nCompound variants are applied after regular variants but before direct prop overrides, allowing for fine-grained control over complex styling rules.\r\n\r\n## API Reference\r\n\r\n### `cva(baseProps, options?): CvaFunction`\r\n\r\nCreates and returns a function that merges base props with variant and compound variant props based on the options provided.\r\n\r\n#### Parameters\r\n\r\n**`baseProps: Props`**\r\n\r\nA table of base properties that are applied to every call. These form the foundation of your component styling.\r\n\r\n```lua\r\nlocal baseProps = {\r\n\tBorderSizePixel = 0,\r\n\tAutoButtonColor = true,\r\n\tFont = Enum.Font.GothamBold,\r\n}\r\n```\r\n\r\n**`options: CvaOptions?` (optional)**\r\n\r\nConfiguration object for defining variants and compound variants.\r\n\r\n##### `options.variants: VariantMap?`\r\n\r\nA table mapping variant names to tables of variant options and their props. Each variant option becomes a choice that consumers of the CVA can select.\r\n\r\n```lua\r\nvariants = {\r\n\t-- Variant name\r\n\tintent = {\r\n\t\t-- Variant option\r\n\t\tprimary = { BackgroundColor3 = Color3.fromRGB(255, 170, 0) },\r\n\t\tsecondary = { BackgroundColor3 = Color3.fromRGB(40, 40, 40) },\r\n\t},\r\n\tsize = {\r\n\t\tsm = { Size = UDim2.fromOffset(120, 36), TextSize = 16 },\r\n\t\tmd = { Size = UDim2.fromOffset(180, 44), TextSize = 20 },\r\n\t\tlg = { Size = UDim2.fromOffset(240, 56), TextSize = 26 },\r\n\t},\r\n}\r\n```\r\n\r\n##### `options.defaultVariants: { [string]: string | number | boolean }?`\r\n\r\nDefault variant selections that are used when a variant is not explicitly provided.\r\n\r\n```lua\r\ndefaultVariants = {\r\n\tintent = \"primary\",\r\n\tsize = \"md\",\r\n}\r\n```\r\n\r\n##### `options.compoundVariants: { CompoundVariant }?`\r\n\r\nA table of compound variant definitions. Each compound variant applies additional props when all of its variant conditions match.\r\n\r\n```lua\r\ncompoundVariants = {\r\n\t{\r\n\t\tintent = \"primary\",\r\n\t\tsize = \"lg\",\r\n\t\tprops = { TextSize = 30 },\r\n\t},\r\n\t{\r\n\t\tintent = \"danger\",\r\n\t\tsize = \"lg\",\r\n\t\tprops = { TextSize = 28 },\r\n\t},\r\n}\r\n```\r\n\r\n#### Returns\r\n\r\nA function with the signature `function(overrides: Props?): Props` that returns a merged props table.\r\n\r\n### Props Merging Order\r\n\r\nWhen you call the returned function with props, they are merged in this order:\r\n\r\n1. **Base props** are applied first (lowest priority)\r\n2. **Default variants** are applied second\r\n3. **Selected variants** override default variants (based on what you pass in)\r\n4. **Matching compound variants** are applied (can override both)\r\n5. **Directly passed props** are merged last (highest priority)\r\n\r\nThis ensures maximum control while maintaining a predictable resolution order:\r\n\r\n```lua\r\nbuttonStyles({\r\n\tintent = \"primary\",\r\n\tsize = \"lg\",\r\n\tBackgroundColor3 = Color3.fromRGB(0, 0, 0), -- This wins\r\n})\r\n-- Final color: RGB(0, 0, 0) from direct override\r\n-- (not from intent variant or compound variant)\r\n```\r\n\r\n## Type Definitions\r\n\r\nCVA exports several types for use in Luau:\r\n\r\n```lua\r\nexport type Props = { [any]: any }\r\n\r\nexport type VariantOption = { [any]: any }\r\n\r\nexport type VariantGroup = { [string]: VariantOption }\r\n\r\nexport type VariantMap = { [string]: VariantGroup }\r\n\r\nexport type CompoundVariant = {\r\n\t[string]: string | number | boolean,\r\n\tprops: Props,\r\n}\r\n\r\nexport type CvaOptions = {\r\n\tvariants: VariantMap?,\r\n\tdefaultVariants: { [string]: string | number | boolean }?,\r\n\tcompoundVariants: { CompoundVariant }?,\r\n}\r\n\r\nexport type CvaFunction = (overrides: Props?) -> Props\r\n\r\nexport type Cva = (baseProps: Props, options: CvaOptions?) -> CvaFunction\r\n```\r\n\r\n## Best Practices\r\n\r\n### Naming Conventions\r\n\r\nUse clear, semantic names for variants:\r\n\r\n```lua\r\n-- Good\r\nvariants = {\r\n\tintent = { primary = {...}, secondary = {...}, danger = {...} },\r\n\tsize = { sm = {...}, md = {...}, lg = {...} },\r\n}\r\n\r\n-- Less clear\r\nvariants = {\r\n\ttheme = { a = {...}, b = {...}, c = {...} },\r\n\tdim = { x = {...}, y = {...}, z = {...} },\r\n}\r\n```\r\n\r\n### Organizing Complex Components\r\n\r\nFor components with many variants, consider grouping related options:\r\n\r\n```lua\r\nlocal buttonStyles = cva(baseProps, {\r\n\tvariants = {\r\n\t\t-- Visual intent\r\n\t\tintent = {\r\n\t\t\tprimary = {...},\r\n\t\t\tsecondary = {...},\r\n\t\t\tdanger = {...},\r\n\t\t\tsuccess = {...},\r\n\t\t},\r\n\t\t-- Size\r\n\t\tsize = {\r\n\t\t\txs = {...},\r\n\t\t\tsm = {...},\r\n\t\t\tmd = {...},\r\n\t\t\tlg = {...},\r\n\t\t\txl = {...},\r\n\t\t},\r\n\t\t-- State\r\n\t\tdisabled = {\r\n\t\t\t[\"true\"] = { Transparency = 0.5, Active = false },\r\n\t\t\t[\"false\"] = { Transparency = 0, Active = true },\r\n\t\t},\r\n\t},\r\n})\r\n```\r\n\r\n### Using Compound Variants for Complex Rules\r\n\r\nUse compound variants instead of deeply nested logic:\r\n\r\n```lua\r\n-- Good: Explicit compound rules\r\ncompoundVariants = {\r\n\t{ size = \"lg\", intent = \"primary\", props = { TextSize = 30 } },\r\n\t{ size = \"lg\", intent = \"danger\", props = { TextSize = 28 } },\r\n}\r\n\r\n-- Less maintainable: Logic inside components\r\nif props.size == \"lg\" and props.intent == \"primary\" then\r\n\tprops.TextSize = 30\r\nend\r\n```\r\n\r\n## Troubleshooting\r\n\r\n### Props Not Applying\r\n\r\nIf a prop isn't appearing in the output, check the resolution order. Direct props always win, so:\r\n\r\n```lua\r\n-- The BackgroundColor3 from intent will be overridden by the direct prop\r\nbuttonStyles({\r\n\tintent = \"primary\",\r\n\tBackgroundColor3 = Color3.fromRGB(100, 100, 100),\r\n})\r\n```\r\n\r\n### Variant Not Recognized\r\n\r\nMake sure the variant value matches exactly (case-sensitive). Typos will be silently ignored:\r\n\r\n```lua\r\nlocal buttonStyles = cva(baseProps, {\r\n\tvariants = {\r\n\t\tsize = {\r\n\t\t\tsm = {...},\r\n\t\t\tmd = {...},\r\n\t\t\tlg = {...},\r\n\t\t},\r\n\t},\r\n})\r\n\r\n-- This works\r\nbuttonStyles({ size = \"lg\" })\r\n\r\n-- This is silently ignored (typo: \"LG\")\r\nbuttonStyles({ size = \"LG\" }) -- Uses default instead\r\n```\r\n\r\n### Compound Variants Not Applying\r\n\r\nEnsure all variant conditions in the compound match exactly. A compound only applies if ALL its conditions are met:\r\n\r\n```lua\r\ncompoundVariants = {\r\n\t{\r\n\t\tintent = \"primary\",\r\n\t\tsize = \"lg\",\r\n\t\tprops = { TextSize = 30 },\r\n\t},\r\n}\r\n\r\n-- Applies compound\r\nbuttonStyles({ intent = \"primary\", size = \"lg\" })\r\n\r\n-- Does NOT apply compound (missing size condition)\r\nbuttonStyles({ intent = \"primary\" })\r\n\r\n-- Does NOT apply compound (only one condition matches)\r\nbuttonStyles({ intent = \"primary\", size = \"md\" })\r\n```\r\n\r\n## Examples\r\n\r\n### Label/Badge Component\r\n\r\n```lua\r\nlocal badgeStyles = cva(\r\n\t{ Font = Enum.Font.GothamBold, TextScaled = true },\r\n\t{\r\n\t\tvariants = {\r\n\t\t\tvariant = {\r\n\t\t\t\tdefault = { BackgroundColor3 = Color3.fromRGB(200, 200, 200) },\r\n\t\t\t\tsuccess = { BackgroundColor3 = Color3.fromRGB(34, 197, 94) },\r\n\t\t\t\twarning = { BackgroundColor3 = Color3.fromRGB(251, 146, 60) },\r\n\t\t\t\terror = { BackgroundColor3 = Color3.fromRGB(239, 68, 68) },\r\n\t\t\t},\r\n\t\t},\r\n\t\tdefaultVariants = {\r\n\t\t\tvariant = \"default\",\r\n\t\t},\r\n\t}\r\n)\r\n```\r\n\r\n### Input Field Component\r\n\r\n```lua\r\nlocal inputStyles = cva(\r\n\t{\r\n\t\tBorderSizePixel = 1,\r\n\t\tBorderColor3 = Color3.fromRGB(200, 200, 200),\r\n\t\tFont = Enum.Font.Gotham,\r\n\t\tTextSize = 14,\r\n\t\tBackgroundColor3 = Color3.fromRGB(255, 255, 255),\r\n\t},\r\n\t{\r\n\t\tvariants = {\r\n\t\t\tstate = {\r\n\t\t\t\tdefault = { BorderColor3 = Color3.fromRGB(200, 200, 200) },\r\n\t\t\t\tfocused = { BorderColor3 = Color3.fromRGB(59, 130, 246) },\r\n\t\t\t\terror = { BorderColor3 = Color3.fromRGB(239, 68, 68) },\r\n\t\t\t},\r\n\t\t\tsize = {\r\n\t\t\t\tsm = { TextSize = 12 },\r\n\t\t\t\tmd = { TextSize = 14 },\r\n\t\t\t\tlg = { TextSize = 16 },\r\n\t\t\t},\r\n\t\t},\r\n\t\tdefaultVariants = {\r\n\t\t\tstate = \"default\",\r\n\t\t\tsize = \"md\",\r\n\t\t},\r\n\t}\r\n)\r\n```\r\n\r\n## Resources\r\n\r\n- **Original CVA** — [cva.style](https://cva.style/)\r\n- **React Lua** — [jsdotlua/react-lua](https://github.com/jsdotlua/react-lua)\r\n- **Roblox** — [roblox.com](https://www.roblox.com)\r\n\r\n## Contributing\r\n\r\nContributions are welcome! Please feel free to submit issues or pull requests to improve CVA.\r\n\r\n## License\r\n\r\nApache-2.0\r\n","readmeTruncated":false}