惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

DEV Community

Fixing the session timeouts Beyond Autonomous AI: Understanding Self-Healing Agents in Enterprise AI Systems MCP Is the AI Platform Camera2 API: Handling Orientation, Focus, and Exposure in Background — How to Keep Your Android Camera Running With the Screen Off I built a free Bitly/TinyURL alternative and self-hosted it on a $6/mo VPS — here's the full stack Stop rebuilding memory and orchestration for every AI agent you build 6 users in one day with zero marketing budget — what actually worked How a photo-blind dating engine actually ranks people (the TypeScript) AI Is Moving From Your Pocket to Your Brain — The 6-Year Timeline I Built a Static Blog Generator in 350 Lines of Python — No Dependencies, No Config, No Nonsense How Does Duolingo Monetize? I Decompiled the Android App (v6.79.5) Next.js Dynamic OG Images: Fix the Turbopack CPU Hang AI Is Turning Every Developer Into an Architect What is props 3 Things Building MediTrack Taught Me About Laravel Vibe Coding: My Daily Workflow with Claude Code Using Python to Do the Wonders: How Flet Changes the Game for Developers OpenDev: From Zero Clients to Linux Independence – How I'm Building a One-Man Linux Revolution Migrating from Jest to Vitest 4: A Complete 2026 Guide Making Equation (2.2) of the OpenAI Erdős Result Executable HTTP request headers: canonical reference Prefix caching in vLLM under multi-tenant agent traffic Introducing Oracle Support in Dory How I built 3 products solo as a CA student using AI — no coding background What is AEO? How to Get ChatGPT, Perplexity & AI Search Engines to Cite Your Website — 2026 Guide HTTP rate-control headers: canonical reference Im attending Manifest 2026! AI Music Doesn’t Need Better Prompts — It Needs Better Systems ORA-00215 오류 원인과 해결 방법 완벽 가이드 Stop Making Your AI Chatbot Slower: Streaming Responses with Spring AI and Server-Sent Events Annotations in Spring Boot What is the Model Context Protocol (MCP)? Gemini CLI Skills: Teaching Your Terminal Agent How to Think 🧠 What the Heck is an API? FairLens AI: An Intelligent Dashboard for Automated Bias Auditing RAG vs Fine-Tuning- Choosing Right Strategy for Modern AI Applications AI Metrics Decoded: From Parameters to TOPS I made git merge finish itself — in VS Code, in my terminal, and in CI You just can’t miss this… Redis Essentials: Architecture, Caching, and Setup Docker with AI: A Practical Guide to Running LLMs, Agents and MCP Design to Code #5: Using AI to Build a Design System Analyzing 1,000 Engineering Problems Through GitHub Data Open Graph protocol: canonical reference How a 400-Engineer SaaS Company Cut PR-to-Production from 4.2 Days to 6.4 Hours with Claude Code Multi-Agent DevOps 💬 Embedded AI Chatbots vs Popup Bubbles — Which One Creates Better Engagement? Bajándole todos los minutos posibles al CI del backend con mas de 1000 tests Harness Engineering: Stop Re-Prompting Your Coding Agent Every Session HTML meta referrer: canonical reference AWS MCP Server Just Gave AI Agents Your Cloud Keys — Here's Why That Should Worry You Announcing the Trust Identity Protocol (TIP): HTTPS for the AI Era We built the feature in two days. Making it reliable took two weeks. LuisCore /for-agents.json — agent bootstrap — daily syndication · 2026-05-26 A Curious Journey Into Reverse Engineering an AI-Generated Python .exe Part 2: Enterprise Decision Intelligence Architecture: AI Governance, Threshold Policy Engines, and Operational AI Systems I will continue using Devise with Rails 8! The Developer's Guide to Picking the Right AI Code Model in 2026 (I Spent $500 So You Don’t Have To) 30 Kubernetes Tasks Every CKA Candidate Should Practice Before Exam Day Why Some Websites Feel Instantly Better to Use Advanced React Patterns I Wish I Knew 5 Years Ago ¿Cómo optimizar algoritmos en arreglos y listas con la técnica de dos punteros? I scanned 8 popular open source repos with one command. Here's what I found. mcp-probe v1.6.0: Stricter GitHub Actions checks for MCP CI gates How we connect two strangers' webcams fast (and keep the TURN bill small) LLM Agents Are Now Finding Zero-Days: How AI is Autonomously Rewriting the Rules of Vulnerability Research Minimal Code Doesn’t Mean Stable Code How I manage 40+ skills across Claude Code, Codex, and .agents folders Hardening Stealth Browser Fingerprint Integrity and State Persistence Quick Tip: Benchmarking Multimodal APIs in Under 10 Minutes How I Slashed My AI API Bill by 92% in 2026 — A Cost Optimizer's Speed Benchmark Guide How I Slashed My AI API Bill by 95% — A Practical Guide for 2026 A Go outbox library that runs inside your own DB transaction How I Built a Credit Optimizer That Saves 30-75% on AI Agent Costs (Open Architecture) The Missing POP: How I Ported a Yul Contract to Huff by Reading Every Opcode The Moment the Config Parser Became the Bottleneck Churn Tool Stack by Revenue Stage ($5K to $50K+) What I Learned Exploring AI-Generated 3D: A Hands-On Tour of Meshy, Tripo, and Three.js Day 15 - Software Composition Analysis(SCA) Contributing Upstream Instead of Forking: My grape-swagger-rails Story Behind The Badge: How We Built 2,000 Hackable Badges For Temporal Replay Access Control Doesn't Scale Linearly -- Part 3 33x faster than Rust: Why I stopped waiting for my compiler and built my own. I Built My First Production AWS Project as a Career Changer Why Detecting PII Matters More Than Ever JSON Schema in 10 Minutes — Validation, Types & Real Examples Python Tasks How I Started My Cybersecurity Journey as an SQA Engineer 🔐 Why "fancy fonts" in Discord and Instagram bios turn into boxes ☁️ GKE private cluster setup — common mistakes and how to avoid them I Thought a Username Didn’t Matter… Until I Saw How Much People Care About It Claude for Small Business: 382K Day-One Buyer's Guide I Built a Diagnostic Toolkit for PyTorch Because I Was Tired of Guessing Why Models Fail How I Built an AI-Powered Incident RCA Platform with LangGraph and RAG The Paywall Was a Painted Door Sonnet hallucinated. My agent stored it as fact. How React-Style Time-Slicing Keeps UIs Responsive 这个 Princeton 开源项目让 AI 自己修 Bug,19K Stars 但 90% 的人只用了 1% 功能 🔥 SWE-agent's 5 Hidden Uses Nobody Told You About 🔥 Decompiling Serial Number U-36: Python TERCOM Reconstruction, Cryptographic Logistical Forensics, and Swarm Consensus Fault Tolerance Microservices Patterns
Design to Code #7: How CVA Scaffolding Turned Into Dead Code
7onic · 2026-05-26 · via DEV Community

The lint config had been sitting in the repo for a week, untouched, when I finally ran it across src/components/ui/ on the afternoon of April 4th. I was expecting maybe a stray console.log, a forgotten TODO — the kind of trivialities you hunt down right before any first publish. What I got back instead was a list of five files where VariantProps was imported but never used: breadcrumb, divider, drawer, pagination, and toast. Fine. Dead imports. Delete them and move on.

But then I opened breadcrumb.tsx and noticed something worse: the cva call itself was also completely unused. Not just the type import — the entire const breadcrumbVariants = cva(...) block was sitting there, fully defined, exported in spirit, and referenced by absolutely nothing. The component rendered its classes via cn() directly. The CVA scaffolding was pure decoration.

I had written that file. Yet I had no memory of writing the CVA part of it, because I had not written it deliberately. I had simply pasted the boilerplate shape of a 7onic component and filled in the middle.

Why CVA Earns Its Slot Elsewhere

Before getting into why removing it from breadcrumb was the right move, it helps to look at why that dependency is everywhere else in the first place.

class-variance-authority is one of the four core packages the 7onic add CLI auto-installs alongside @​7onic-ui/tokens, clsx, and tailwind-merge. Four dependencies. That is the absolute floor of this design system. If you install a single 7onic component, all four show up in your node_modules, and CVA is the only one that isn't either a pure utility (clsx, tailwind-merge) or the design token layer itself.

It earns that real estate mostly on components like Button.

The Button component has, in its current form, four variants (solid, outline, ghost, link), six sizes (xs, sm, md, default, lg, icon), nine radius values (none through full), and a fullWidth boolean. Multiply that out and you get something north of four hundred valid combinations, every single one of which needs to produce the exact right Tailwind class string. Without CVA, the alternative is a long, miserable staircase of nested ternaries and template literals, or some homegrown lookup map that re-implements the same idea worse. The CVA call collapses that entire matrix into a single declarative object:

const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap transition-all duration-micro focus-visible:focus-ring disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: { solid: 'font-semibold', outline: 'border border-border bg-background text-foreground hover:bg-background-muted', ghost: '...', link: '...' },
      size: { xs: 'h-7', sm: 'h-8', md: 'h-9', default: 'h-10', lg: 'h-12', icon: 'h-10 w-10' },
      radius: { none: '...', /* ...nine total */ full: '...' },
      fullWidth: { true: 'w-full' },
    },
    defaultVariants: { variant: 'solid', size: 'default', radius: 'default', fullWidth: false },
  }
)

Enter fullscreen mode Exit fullscreen mode

But handling class strings is only the smaller half of what CVA actually does for the library.

The Part That Isn't About Classes

The real reason CVA is worth a dedicated dependency slot is the line immediately following that cva call:

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants> & { /* ... */ }

Enter fullscreen mode Exit fullscreen mode

VariantProps<typeof buttonVariants> reads the variants object and infers the prop types directly from it. variant automatically becomes 'solid' | 'outline' | 'ghost' | 'link'. size becomes 'xs' | 'sm' | 'md' | 'default' | 'lg' | 'icon'. If I add a new size next month, the type system updates without me ever touching the props interface. If I rename solid to filled, every consumer site gets a red compile error the moment they pull the updated version.

You can easily build the class-string concatenation machinery yourself in twenty lines of code and tree-shake your way out of the dependency. What you cannot easily replicate yourself is this bidirectional link between the configuration object and the TypeScript types. CVA's true value lives inside the type system; the runtime class composition is just along for the ride.

Which is exactly what made the breadcrumb situation slightly ridiculous. Breadcrumb had a cva call with variants: {} — empty object, no variants — and it still had VariantProps imported at the top of the file. VariantProps<typeof breadcrumbVariants> resolves to {}. Zero props. The entire setup was a circle whose circumference was exactly zero.

When Variants Aren't Enough

The other thing worth knowing about CVA, before addressing the problem with defaults, is that it has a known soft edge. CVA fundamentally expects each variant to be orthogonal — meaning size should be independent of variant, which should be independent of radius. The exact moment you introduce a prop that only makes sense in tight combination with another prop, you step outside the comfortable middle of the library.

Button's color prop is the clearest example. It accepts default, primary, secondary, or destructive, and it only actually does anything when variant === 'solid'. An outline button doesn't have a solid fill color in the same way; a ghost button doesn't either. If I were to put color inside the main CVA variants object, it would show up in the TypeScript types for all variants. Consumers could legally write <Button variant="ghost" color="destructive" /> and get something that compiles cleanly but produces incoherent runtime styling.

So color lives outside CVA as a plain JavaScript lookup map:

const solidColorMap = {
  default: '...',
  primary: '...',
  secondary: '...',
  destructive: '...',
}

Enter fullscreen mode Exit fullscreen mode

And the final class assembly looks like this:

cn(
  buttonVariants({ variant, size, radius, fullWidth }),
  variant === 'solid' && solidColorMap[color],
  className
)

Enter fullscreen mode Exit fullscreen mode

The variant === 'solid' && ... line is load-bearing. CVA hands back the base utility classes; the conditional hands back the contextual color, but only when it is strictly relevant; cn flattens everything out while resolving any Tailwind utility conflicts. It's not exceptionally beautiful, but it's an architectural seam. CVA handles the clean, rectangular center of the design space and the conditional patches the corners.

For a long time I wanted to push color back into CVA via compoundVariants — the specific API designed for cross-prop interactions. I never found a way to express "this prop only exists when this other prop has this specific value" through compoundVariants. It can apply extra utility classes when two variants combine, but it cannot make a prop conditionally part of the TypeScript type interface itself. So the escape hatch remains.

Where compoundVariants Does Pull Its Weight

Five of the 42 components use compoundVariants: divider, tabs, segmented, textarea, and input. These are the components where the cross-prop logic isn't "should this prop exist?" but rather "what specific class combination does this intersection produce?"

The clearest use case is Input, which maps out three entries:

compoundVariants: [
  { focusRing: true, className: 'focus-visible:shadow-[0_0_0_2px_var(--color-focus-ring)]' },
  { variant: 'default', focusRing: false, className: 'focus:border-border-strong' },
  { variant: 'filled', state: 'error', className: 'border-transparent ... bg-[var(--color-error-bg)]' },
]

Enter fullscreen mode Exit fullscreen mode

The third entry is the interesting one. variant: 'filled' and state: 'error' are both perfectly legal in isolation. A filled input without an error renders with a subtle gray background. An outline input with an error renders with a stark red border. But the combination of filled and errored needs its own custom treatment — a transparent border (because there is no outline to color) and a red-tinted background instead of the standard gray. Without compoundVariants you would either need a third state value (filled-error?) or lift the conditional outside CVA, creating the exact same seam I had to build for Button's color.

Divider's compoundVariants block is less complex but significantly more dense — ten entries covering the cross product of orientation (horizontal/vertical) with spacing (sm/md/default/lg). Each combination produces the correct margin utility and border direction. horizontal + default yields border-t my-4. vertical + sm yields border-l mx-2. The entire matrix laid out with zero conditionals at the call site.

These are the exact use cases CVA was actually engineered for.

What the Lint Sweep Was Actually Saying

So, the tally stood at five files with unused VariantProps. Four of them — divider, drawer, pagination, and toast — were using CVA productively but had simply forgotten to clean up the type import lines after a previous refactoring pass. An easy delete.

The fifth file, breadcrumb, was the one where I had to admit something to myself. Breadcrumb genuinely does not have variants. It has a rigid structure — a list of items separated by a visual separator — and the only thing that changes between instances is the literal content. There is no size prop because the size is inherited from the surrounding text context. There is no variant prop because there is only one way to render a breadcrumb in this system. The component is a thin, sensible wrapper around <nav> and <ol>.

Yet I had still given it a full cva call. With a completely empty variants object. Simply because every other file in the library had one.

That is the part worth writing down. The CLI installs CVA. The component templates start with CVA. Forty-two components use CVA. So when I sat down to author the breadcrumb file, my hands typed import { cva, type VariantProps } from 'class-variance-authority' before my brain ever caught up with the fact that there were no variants to manage. The lint sweep wasn't merely catching dead code; it was catching the underlying assumption that the pattern itself is the point.

The final result, after the cleanup: 0 errors, 0 warnings. Five files lighter on unused imports. One file lighter on an entire unused cva block. And a small, sobering note left in the ADR: "Pattern boilerplate vs deliberate use: not the same thing."

I think about that note more than I want to. The whole appeal of building a component library, from the inside, is that you stop having to make tiny, repetitive decisions. You reach for the established template, fill in the middle, and ship. The hidden cost is that you eventually stop noticing when the template is making the decisions for you. CVA inside the breadcrumb wasn't a load-bearing tool. It was just a habit wearing the costume of one.


Next: 7onic ships one entry point. No @​7onic-ui/react/button subpath imports. The reason is embarrassingly specific to one incident where generateCode() sent users to an import path that didn't exist.


About 7onic — An open-source React design system where design and code never drift. Free, MIT licensed. Docs and interactive playground at 7onic.design. Source code on GitHub — stars appreciated. More posts in this series at blog.7onic.design. Follow updates on X at @​7onicHQ.