Why I separated `variant` from `intent` in my component API — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogWhy I separated `variant` from `intent` in my component API
    Back to Blog
    Why I separated `variant` from `intent` in my component API
    react

    Why I separated `variant` from `intent` in my component API

    Sinisa Kusic April 6, 2026
    0 views

    Most component libraries conflate visual weight and semantic meaning into a single prop. Here is why that is a mistake and how to fix it.

    --- title: Why I separated `variant` from `intent` in my component API published: true description: Most component libraries conflate visual weight and semantic meaning into a single prop. Here is why that is a mistake and how to fix it. tags: react, typescript, designsystems, webdev # cover_image: https://direct_url_to_image.jpg # Use a ratio of 100:42 for best results. # published_at: 2026-04-06 10:06 +0000 --- Every component library starts the same way. You add a Button. It needs a primary style and a danger style, so you reach for a `variant` prop. Simple enough. Then someone needs a ghost button that also signals danger. You add `variant="ghost-danger"`. Then outline-success. Then link-warning. Then secondary-danger. Each new combination feels reasonable in isolation. Six months later you have a prop that accepts seventeen strings, half of which your consumers will never discover because nothing in the type system points them there. This is not a tooling problem. It is a modeling problem. The `variant` prop is doing two unrelated jobs, and conflating them is what causes the explosion. --- ## Two orthogonal concerns When you look at what `variant` is actually encoding in most button APIs, it is carrying two distinct signals: **Visual weight**: how much attention the component demands. Primary buttons are loud. Ghost buttons are quiet. Outline sits between them. This is a presentation decision. **Semantic meaning**: what the action communicates. Danger means destructive. Success means confirmation. Warning means proceed with caution. This is a communication decision. These are orthogonal axes. A ghost button can be dangerous. An outline button can confirm success. There is no inherent relationship between how loud a button is and what it means. Cramming both signals into a single prop forces a false coupling that grows more expensive with every variant you add. The fix is to give each concern its own prop. --- ## The split In nuka-ui, my open-source React component library built on Tailwind v4, the API separates these into two independent props: A note on the project itself. I built nuka-ui because I kept repeating the same patterns across every project I started, personal, hobby, showcase, whatever. Rebuilding accessible, production-ready components from scratch every time is a real burden, and I got tired of it. The library exists to solve that repetition once, with accessibility and solid UX baked in from the start rather than bolted on later. It is not on npm yet. Navigation and Composites are the remaining pieces before the first publish. - `variant` controls visual weight: `primary`, `secondary`, `outline`, `ghost`, `link` - `intent` controls semantic meaning: `default`, `danger`, `success`, `warning` Every combination is valid. The consumer API looks like this: ```tsx <Button variant="primary" intent="default" /> <Button variant="ghost" intent="danger" /> <Button variant="outline" intent="success" /> <Button variant="secondary" intent="warning" /> ``` Compare that to the flat approach: ```tsx <Button variant="ghost-danger" /> <Button variant="outline-success" /> <Button variant="secondary-warning" /> ``` The flat approach is not unreadable. The problem is discoverability and scale. A consumer looking at `variant`'s type has no way to know which combinations exist, which are intentional, and which ones you simply never got around to building. The two-prop model makes the full space explicit and uniform. Every variant works with every intent. There are no gaps. --- ## How CVA implements the intersection Separating the props does not make the styling simpler. It makes it honest. You still need to define what every combination looks like. That work is done through CVA's `compoundVariants`: ```tsx const buttonVariants = cva(baseClasses, { variants: { variant: { primary: [], secondary: [], outline: [], ghost: [], link: [], }, intent: { default: "", danger: "", success: "", warning: "", }, }, compoundVariants: [ { variant: "primary", intent: "default", className: [ "bg-[var(--nuka-accent-bg)]", "text-[var(--nuka-text-inverse)]", "hover:bg-[var(--nuka-accent-bg-hover)]", ], }, { variant: "primary", intent: "danger", className: [ "bg-[var(--nuka-danger-base)]", "text-[var(--nuka-text-inverse)]", "hover:brightness-90", ], }, { variant: "ghost", intent: "danger", className: [ "text-[var(--nuka-danger-text)]", "hover:bg-[var(--nuka-danger-bg)]", ], }, // one entry per variant x intent combination ], defaultVariants: { variant: "primary", intent: "default", size: "md", }, }) ``` 5 variants multiplied by 4 intents gives you 20 compound variant entries for Button alone. You write all 20 explicitly. That is the cost of a complete, deliberate API, and it is a one-time authoring cost. Once they are written, the intersection is fully covered. Adding a consumer use case requires no library changes, just passing the two props you already have. TypeScript enforces the contract at compile time: ```tsx interface ButtonProps extends ButtonVariantProps { variant?: "primary" | "secondary" | "outline" | "ghost" | "link" intent?: "default" | "danger" | "success" | "warning" } ``` Invalid prop values do not survive a build. The type system reflects the actual API surface, not a historical accident of how variants accumulated. --- ## What you gain at scale The pattern compounds across a library. In nuka-ui it applies to Button, Alert, Badge, Tag, Code, Input, and Checkbox. Each component defines its own variant and intent axes and handles the intersections through compound variants. The mental model is consistent everywhere. A consumer who understands how Button works understands how Alert works. Adding a new intent, say `info`, requires adding N compound variant entries per component, where N is the number of that component's variants. It is more work than adding a single flat variant string, but the scope is bounded and predictable. You know exactly what needs to be done, and you cannot accidentally miss a combination because the grid is explicit. --- ## Tradeoffs, and when not to apply this The verbosity is real. Twenty compound variant entries per component is a lot of lines. If your library is small and your variant space is stable, the flat approach is less overhead and probably fine. This pattern earns its cost at scale, when the alternative is a `variant` prop with an ever-growing string union that no one can hold in their head. Consumers also have to understand two props instead of one. For most senior engineers this is a non-issue. The separation is intuitive once named. For a component library targeting less experienced consumers, the additional concept may need more documentation investment. The pattern also does not apply universally. Banner in nuka-ui uses `intent` alone. There is no `variant` prop because Banner has one visual weight. Applying the full grid to a component with a single presentation mode would be mechanical pattern application, not design. The question to ask is whether the component genuinely has independent visual weight and semantic axes. If it does not, use the simpler model. Two alternatives worth naming explicitly, because I considered both before landing here: **Flat variants** are simpler to implement initially. The explosion problem only becomes painful as the library grows, which is exactly when you have the least appetite to refactor the API. **CSS data attributes** (`data-intent="danger"`) avoid prop surface area but lose TypeScript type safety and make the API implicit. The props approach is more explicit and more tool-friendly. --- ## Where to see it in practice The full implementation is in the nuka-ui repository at https://github.com/ku5ic/nuka-ui. The live Storybook at https://ku5ic.github.io/nuka-ui shows every variant and intent combination across all components. If you want to follow along as the library moves toward its first npm publish, starring the repo is the easiest way to keep up with progress. Most component API problems are modeling problems in disguise. This one just happens to be easy to see once you know where to look.

    Tags

    reacttypescriptdesignsystemswebdev

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.