# How to Capture Multi-State Component Behaviors: Tabs, Hover Effects, and ARIA States in React

> Learn to capture multi-state component behaviors in React using Tailwind CSS for hover effects and class-variance-authority with ARIA for tabs. Simplify UI state management without custom JS.

- Repository: [JCodesMore/ai-website-cloner-template](https://github.com/JCodesMore/ai-website-cloner-template)
- Tags: how-to-guide
- Published: 2026-07-03

---

**Use Tailwind CSS pseudo-class utilities for hover effects and combine `class-variance-authority` with ARIA attributes to declaratively manage complex UI states like tabs without writing custom JavaScript event handlers.**

The `JCodesMore/ai-website-cloner-template` repository demonstrates a modern, declarative approach to capturing multi-state component behaviors using utility-first CSS and accessibility-driven HTML attributes. By leveraging Tailwind's conditional modifiers and the `cva` helper function, you can build interactive React components that respond to hover, focus, and selected states while maintaining full type safety and semantic markup.

## Understanding the State Management Architecture

The template handles UI state through three core technologies: **Tailwind CSS**, **ARIA attributes**, and **class-variance-authority (cva)**. This combination eliminates the need for manual CSS-in-JS or complex useEffect hooks to track visual states.

In [`src/components/ui/button.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/components/ui/button.tsx), the component defines visual variants using the `cva` utility. The `buttonVariants` configuration includes Tailwind's `hover:` utilities (such as `hover:bg-primary/80` and `hover:bg-muted`) directly in the class string. When the component renders with `className={cn(buttonVariants({ … }))}`, the Tailwind compiler transforms these utility classes into responsive CSS pseudo-classes, requiring zero additional JavaScript to handle mouse interactions.

## Capturing Hover Effects with Tailwind Utilities

Hover states are implemented purely through Tailwind's **pseudo-class modifiers**. The button component in [`src/components/ui/button.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/components/ui/button.tsx) demonstrates this pattern by appending hover variants to the base class definitions.

```typescript
// From src/components/ui/button.tsx
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-muted hover:text-muted-foreground",
      },
    },
  }
);

```

The `cn()` utility from [`src/lib/utils.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/lib/utils.ts) safely merges these class strings with any custom className props passed at runtime. Because Tailwind processes these `hover:` prefixes at build time, the browser applies the styles instantly without JavaScript execution overhead.

## Building Multi-State Tabs with ARIA Attributes

For complex multi-state behaviors like **tabs**, the template uses ARIA attributes combined with Tailwind's **attribute selectors**. This approach keeps state management declarative and accessible.

The pattern works by toggling ARIA attributes (such as `aria-selected`) based on component state, then targeting those attributes with Tailwind's bracket syntax: `[aria-selected="true"]:`. This mirrors the implementation strategy found in [`src/components/ui/button.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/components/ui/button.tsx) but extends it for custom state logic.

### Implementing the Tab Component

Create a new file at [`src/components/ui/tab.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/components/ui/tab.tsx) using the `cva` pattern to define state-dependent styling:

```tsx
"use client";

import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Button } from "./button";

const tabVariants = cva(
  "group/tab inline-flex items-center justify-center border-b-2 transition-colors",
  {
    variants: {
      selected: {
        true: "border-primary text-primary",
        false: "border-transparent text-muted-foreground hover:text-foreground",
      },
    },
    defaultVariants: {
      selected: false,
    },
  }
);

type TabProps = {
  isSelected: boolean;
  onClick: () => void;
  children: React.ReactNode;
} & VariantProps<typeof tabVariants>;

export function Tab({ isSelected, onClick, children, ...rest }: TabProps) {
  return (
    <Button
      role="tab"
      aria-selected={isSelected}
      className={cn(tabVariants({ selected: isSelected }), rest.className)}
      onClick={onClick}
    >
      {children}
    </Button>
  );
}

```

The `aria-selected={isSelected}` prop drives the styling logic while ensuring accessibility. When the attribute equals `"true"`, the `selected` variant applies the active styling classes, creating a fully accessible tab interface that screen readers can navigate correctly.

### Using the Tab Component in Practice

Implement the tab list in your page components by managing state with React's useState hook:

```tsx
import { useState } from "react";
import { Tab } from "@/components/ui/tab";

export function TabList() {
  const [active, setActive] = useState(0);
  const tabs = ["Home", "Features", "Pricing"];

  return (
    <nav role="tablist" className="flex gap-4 border-b border-border">
      {tabs.map((label, idx) => (
        <Tab
          key={label}
          isSelected={active === idx}
          onClick={() => setActive(idx)}
        >
          {label}
        </Tab>
      ))}
    </nav>
  );
}

```

This implementation inherits hover styling from the underlying `Button` component via the `hover:text-foreground` utility, while the `selected` variant handles the active state visualization through the ARIA attribute selector.

## The class-variance-authority Pattern Explained

The **`cva` utility** groups related variant classes into type-safe configurations. It accepts a base class string and a variants object containing conditional class maps. When called with specific variant props, it returns a merged class string that `cn()` then combines with any runtime className overrides.

Key characteristics of this pattern:

- **Type safety**: The `VariantProps<typeof tabVariants>` type ensures TypeScript validates your variant props
- **Composability**: Variants stack (e.g., combining `variant="primary"` with `size="lg"`) automatically
- **Default fallbacks**: The `defaultVariants` configuration ensures predictable styling when props are omitted

## Summary

- **Tailwind pseudo-classes** (`hover:`, `focus-visible:`) handle mouse and keyboard interactions without JavaScript in files like [`src/components/ui/button.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/components/ui/button.tsx)
- **ARIA attributes** combined with bracket selectors (`[aria-selected="true"]`) manage complex component states declaratively
- **class-variance-authority** provides type-safe variant definitions that integrate seamlessly with the `cn()` utility from [`src/lib/utils.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/lib/utils.ts)
- The pattern supports full accessibility compliance by using semantic HTML attributes like `role="tab"` and `aria-selected`

## Frequently Asked Questions

### How does the cn() utility prevent class conflicts when merging variants?

The `cn()` function from [`src/lib/utils.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/lib/utils.ts) uses `tailwind-merge` and `clsx` to intelligently combine class strings. It automatically resolves conflicting Tailwind utilities by prioritizing the last defined class, ensuring that custom className props override base variants without causing duplicate CSS.

### Can I use this pattern for states like loading or disabled?

Yes. Extend the `cva` variants object to include `loading` or `disabled` states, then drive them via ARIA attributes (`aria-disabled`, `aria-busy`) or data attributes. Target these with Tailwind selectors like `[aria-disabled="true"]:opacity-50` or `data-[loading=true]:animate-pulse` to keep the styling fully declarative and accessible.

### Is this approach performant for large lists of interactive elements?

This approach is highly performant because all styling calculations happen at build time. The Tailwind compiler generates static CSS for the `[aria-selected]` and `hover:` prefixes, so the browser applies these styles natively without JavaScript execution overhead during runtime state changes.

### How do I customize the base Tailwind configuration for these states?

Modify the global Tailwind configuration in [`src/app/layout.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/app/layout.tsx) or your [`tailwind.config.ts`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/tailwind.config.ts) to define custom colors and animations. Ensure your theme tokens (like `primary`, `muted`, `foreground`) are defined in the Tailwind config so that utilities like `hover:bg-primary/80` and `text-muted-foreground` resolve correctly at build time.