Why I separated `variant` from `intent` in my component API…
    Neura MarketNeura Market/Stable Diffusion
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityStable DiffusionStable Diffusion
    DeepSeekDeepSeekCoPilotCoPilotMidjourneyMidjourney
    View All Directories
    OverviewPromptsBlogVideosGuidesCoursesCommunityModelsLoRAsComfyUI WorkflowsTrending
    Stable DiffusionBlogWhy 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:

    <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:

    <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:

    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:

    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
    Context bankruptcy: The case for strategic forgetting for AI Agentsai

    Context bankruptcy: The case for strategic forgetting for AI Agents

    Most of us have seen a coding agent fail to complete a task we know it can do. We just don't...

    J
    James O'Reilly
    Parallel Compliance Engine: Drive-to-Sheets Multi-Agent Orchestrationgooglecloud

    Parallel Compliance Engine: Drive-to-Sheets Multi-Agent Orchestration

    When building Generative AI applications, developers often encounter a massive bottleneck: sequential...

    A
    Aryan Irani
    Is It Ethical to Post and Ask About Circuits on Dev.to?discuss

    Is It Ethical to Post and Ask About Circuits on Dev.to?

    I’ve been thinking about sharing some electronic circuit posts on Dev.to — small circuits, DIY...

    C
    codebunny20
    The One-Click Exporter: AI Studio Antigravity, Probed to Its Limitsagents

    The One-Click Exporter: AI Studio Antigravity, Probed to Its Limits

    What nobody tells you about exporting your multi-agent prototype to a local workspace. Every...

    L
    leslysandra
    Guarding the till while autonomous data agents do the diggingagenticarchitect

    Guarding the till while autonomous data agents do the digging

    Autonomous agents are genuinely good at answering messy business questions. Give one an LLM and a set...

    S
    Sireesha Pulipati
    Return on Attention: Why AI Code Reviews Are Wearing Us Outai

    Return on Attention: Why AI Code Reviews Are Wearing Us Out

    PR volume went up, ticket quality didn't, and the gap got filled with LLMs on both sides of the review: bots reviewing, bots replying, bots occasionally arguing with bots about priorities that only existed in a teammate's head. Our CEO named the actual problem, and it's bigger than code review.

    C
    christine

    Stay up to date

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

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for Stable Diffusion 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.

    Ready-made automations for this

    Workflows from the Neura Market marketplace related to this Stable Diffusion resource

    • Extract text from pdf (component)pipedream · Free · Related topic
    • Extract Business Leads from Google Maps with Dumpling AI to Google Sheetsn8n · Free · Related topic
    • Extract Text from Images & PDFs via Telegram with Mistral OCR to Markdownn8n · Free · Related topic
    • Cold Email Icebreakers from Local Business Search with GP-4 and Dumpling AIn8n · Free · Related topic
    Browse all workflows