Skip to main content
CVA integrates with Svelte’s component model. Define your CVA function in the <script> block and pass its output to the class attribute. Use $$props.class to forward any extra classes passed by the consumer.

Installation

npm install cva

Button component

A complete Svelte button component using CVA with scoped CSS:
button.svelte
<script lang="ts">
  import type { HTMLButtonAttributes } from "svelte/elements";
  import { cva, type VariantProps } from "cva";

  const button = cva({
    base: "button",
    variants: {
      intent: {
        primary: "primary",
        secondary: "secondary",
      },
      size: {
        small: "small",
        medium: "medium",
      },
      disabled: {
        false: "enabled",
        true: "disabled",
      },
    },
    compoundVariants: [
      { intent: "primary", size: "medium", class: "primaryMedium" },
    ],
  });

  interface $$Props extends HTMLButtonAttributes, Omit<VariantProps<typeof button>, 'disabled'> {}

  /**
   * For Svelte components, we recommend setting your defaultVariants within
   * Svelte props (which are `undefined` by default)
   */
  export let intent: $$Props["intent"] = "primary";
  export let size: $$Props["size"] = "medium";
  export let disabled: $$Props["disabled"] = false;
</script>

<button
  {...$$props}
  class={button({ intent, size, disabled: disabled ?? false, class: $$props.class })}
  {disabled}
>
  <slot />
</button>

<style>
  .button {
    display: inline-flex;
    border-width: 1px;
    border-style: solid;
  }

  .primary {
    color: rgb(255 255 255);
    background-color: rgb(59 130 246);
    border: transparent;
  }

  .primary.enabled:hover {
    background-color: rgb(37 99 235);
  }

  .secondary {
    background-color: rgb(255 255 255);
    color: rgb(31 41 55);
    border-color: rgb(156 163 175);
  }

  .secondary.enabled:hover {
    background-color: rgb(243 244 246);
  }

  .small {
    font-size: 0.875rem /* 14px */;
    line-height: 1.25rem /* 20px */;
    padding: 0.25rem 0.5rem;
  }

  .medium {
    font-size: 1rem /* 16px */;
    line-height: 1.5rem /* 24px */;
    padding: 0.5rem 1rem;
  }

  .primaryMedium {
    text-transform: uppercase;
  }

  .disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
</style>
In Svelte, set default variant values directly on the exported props rather than in defaultVariants. This follows Svelte’s idiomatic prop pattern where exported props default to undefined. The $$Props interface uses Omit<VariantProps<typeof button>, 'disabled'> to avoid conflicting with Svelte’s native HTMLButtonAttributes disabled type.

Forwarding class from the consumer

To support consumers passing extra classes via class="...", use $$props.class when calling the CVA function:
<button
  {...$$props}
  class={button({ intent, size, disabled: disabled ?? false, class: $$props.class })}
  {disabled}
>
  <slot />
</button>
Spreading {...$$props} first and then explicitly setting class ensures the CVA-generated class string takes precedence over the raw class prop in $$props.

Consuming the component

App.svelte
<script lang="ts">
  import Button from "./components/button.svelte";
</script>

<!-- Uses prop defaults (primary, medium, enabled) -->
<Button>Click me</Button>

<!-- Explicit variants -->
<Button intent="secondary" size="small">Cancel</Button>

<!-- Disabled -->
<Button disabled>Unavailable</Button>

<!-- Extra class forwarded to CVA -->
<Button class="mt-4">With extra class</Button>

Build docs developers (and LLMs) love