Skip to Content
styled-cva logostyled-cva

Usage Examples

Common patterns and real-world examples using styled-cva.

Common patterns and real-world examples using styled-cva.

Intrinsic CVA shorthand

Preferred over tw.button.cva(base, config) (deprecated on intrinsics — same behavior):

const Button = tw.button(
  "inline-flex items-center justify-center rounded-md font-medium",
  {
    variants: {
      $variant: {
        primary: "bg-primary text-primary-foreground",
        secondary: "bg-secondary text-secondary-foreground",
      },
    },
    defaultVariants: { $variant: "primary" },
  },
);

Buttons

Basic Button with Variants

A standard button component with intent and size variants.

import tw from "@styled-cva/react";

const Button = tw.button(
  "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
  {
    variants: {
      $variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "underline-offset-4 hover:underline text-primary",
      },
      $size: {
        default: "h-10 py-2 px-4",
        sm: "h-9 px-3 rounded-md",
        lg: "h-11 px-8 rounded-md",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      $variant: "default",
      $size: "default",
    },
  },
);

export default function ButtonExample() {
  return (
    <div className="flex flex-wrap gap-4 p-4">
      <Button>Default</Button>
      <Button $variant="secondary">Secondary</Button>
      <Button $variant="destructive">Destructive</Button>
      <Button $variant="outline">Outline</Button>
      <Button $variant="ghost">Ghost</Button>
      <Button $variant="link">Link</Button>
    </div>
  );
}

Form Elements

Input with Validation States

An input component that handles error, success, and disabled states.

import tw from "@styled-cva/react";

const Input = tw.input(
  "flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
  {
    variants: {
      $state: {
        default: "border-input focus-visible:ring-ring",
        error:
          "border-red-500 focus-visible:ring-red-500 text-red-900 placeholder:text-red-300",
        success: "border-green-500 focus-visible:ring-green-500 text-green-900",
      },
    },
    defaultVariants: {
      $state: "default",
    },
  },
);

export default function InputExample() {
  return (
    <div className="grid w-full max-w-sm items-center gap-4">
      <Input placeholder="Default input" />
      <Input
        $state="error"
        placeholder="Error state"
        defaultValue="Invalid input"
      />
      <Input
        $state="success"
        placeholder="Success state"
        defaultValue="Valid input"
      />
      <Input disabled placeholder="Disabled input" />
    </div>
  );
}

Interactive Components

Toggle Switch

A switch component using aria-pressed or a custom prop for state.

import { useState } from "react";
import tw from "@styled-cva/react";

const SwitchRoot = tw.button(
  "peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
  {
    variants: {
      $checked: {
        true: "bg-primary",
        false: "bg-input",
      },
    },
  },
);

const SwitchThumb = tw.span(
  "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform",
  {
    variants: {
      $checked: {
        true: "translate-x-5",
        false: "translate-x-0",
      },
    },
  },
);

export default function SwitchExample() {
  const [checked, setChecked] = useState(false);

  return (
    <SwitchRoot
      $checked={checked}
      onClick={() => setChecked(!checked)}
      role="switch"
      aria-checked={checked}
    >
      <SwitchThumb $checked={checked} />
    </SwitchRoot>
  );
}

Layouts

Card

A composed card component.

import tw from "@styled-cva/react";

const Card = tw.div`rounded-lg border bg-card text-card-foreground shadow-sm`;
const CardHeader = tw.div`flex flex-col space-y-1.5 p-6`;
const CardTitle = tw.h3`text-2xl font-semibold leading-none tracking-tight`;
const CardDescription = tw.p`text-sm text-muted-foreground`;
const CardContent = tw.div`p-6 pt-0`;
const CardFooter = tw.div`flex items-center p-6 pt-0`;

export default function CardExample() {
  return (
    <Card className="w-[350px]">
      <CardHeader>
        <CardTitle>Create project</CardTitle>
        <CardDescription>Deploy your new project in one-click.</CardDescription>
      </CardHeader>
      <CardContent>
        <p>Project details form goes here...</p>
      </CardContent>
      <CardFooter className="flex justify-between">
        <button className="text-sm">Cancel</button>
        <button className="bg-primary rounded px-4 py-2 text-white">
          Deploy
        </button>
      </CardFooter>
    </Card>
  );
}

Polymorphic Components

Using the $as prop to render a button as a link.

import Link from "next/link";
import tw from "@styled-cva/react";

const Button = tw.button(
  "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2",
);

export default function PolymorphicExample() {
  return (
    <div className="flex gap-4">
      {/* Renders as <button> */}
      <Button onClick={() => alert("Clicked!")}>Button</Button>

      {/* Renders as <a> */}
      <Button
        $as="a"
        href="https://github.com/alanrsoares/styled-cva"
        target="_blank"
        rel="noreferrer"
      >
        GitHub Link
      </Button>

      {/* Renders as Next.js Link */}
      <Button $as={Link} href="/docs/getting-started">
        Get Started
      </Button>
    </div>
  );
}

Type-safe wrapper for custom components

When you want explicit typing for a custom-component $as (e.g. so consumers see autocomplete for both the link's props and your CVA variants), use PolymorphicComponentProps to compose the prop type:

import Link, { type LinkProps } from "next/link";
import tw, { type PolymorphicComponentProps } from "@styled-cva/react";

const Button = tw.button("btn", {
  variants: {
    $variant: { primary: "btn-primary", secondary: "btn-secondary" },
  },
});

type ButtonLinkProps = PolymorphicComponentProps<typeof Button, typeof Link> &
  LinkProps;

export const ButtonLink = (props: ButtonLinkProps) => (
  <Button {...props} $as={Link} />
);

// <ButtonLink href="/about" $variant="primary">About</ButtonLink>

PolymorphicComponentProps automatically picks up every $-prefixed variant prop on the source component, so renaming or adding variants doesn't require updating the wrapper's type.

Advanced Composition

Extending Components

Styling a custom component or extending an existing one.

import tw from "@styled-cva/react";

// Base component
const BaseBox = tw.div`p-4 border rounded`;

// Extended component overrides styles
const WarningBox = tw(BaseBox)`border-yellow-500 bg-yellow-50 text-yellow-900`;

// Custom React Component
const UserProfile = ({
  className,
  name,
}: {
  className?: string;
  name: string;
}) => (
  <div className={className}>
    <span className="font-bold">{name}</span>
  </div>
);

// Styled Custom Component
const StyledProfile = tw(
  UserProfile,
)`flex items-center gap-2 p-2 bg-gray-100 rounded-full`;

export default function AdvancedCompositionExample() {
  return (
    <div className="flex flex-col gap-4">
      <BaseBox>Base Box</BaseBox>
      <WarningBox>Warning Box</WarningBox>
      <StyledProfile name="John Doe" />
    </div>
  );
}

Default Props

Pre-configuring Components

Using .withProps() to set default attributes and variant values.

import tw from "@styled-cva/react";

const IconButton = tw.button(
    "inline-flex items-center justify-center rounded-full p-2 transition-colors",
    {
      variants: {
        $variant: {
          primary: "bg-blue-600 text-white hover:bg-blue-700",
          ghost: "hover:bg-gray-100 text-gray-600",
        },
      },
    },
  )
  .withProps({
    type: "button",
    $variant: "primary",
    "aria-label": "Icon button",
  });

export default function DefaultPropsExample() {
  return (
    <div className="flex gap-4">
      {/* Uses all defaults */}
      <IconButton>
        <span className="size-4">🚀</span>
      </IconButton>

      {/* Overrides default variant */}
      <IconButton $variant="ghost">
        <span className="size-4">⚙️</span>
      </IconButton>

      {/* Overrides default aria-label */}
      <IconButton aria-label="Settings">
        <span className="size-4">🛠️</span>
      </IconButton>
    </div>
  );
}

On this page