---
name: radii-component
description: Review any UI component for state coverage, variant completeness, motion design, anatomy, and accessibility — with copy-paste JSX for missing states
version: 3.0.0
author: radii.cloud
---

# Radii Component

Review the provided UI component against Radii's component standards. Check anatomy completeness, variant coverage, all interactive states, motion design quality, and accessibility. Return a structured, executable checklist — and for every missing state, output ready-to-paste JSX to add it.

## Required states (by component type)

**All interactive components:**
- **Default** — resting appearance
- **Hover** — feedback on mouse-over; use transition 150ms ease-out
- **Active/pressed** — scale(0.97) + darker color, onset 80ms
- **Focus-visible** — 2px ring, 3:1 contrast against adjacent background, `focus-visible:` selector only
- **Disabled** — opacity 0.4–0.5 OR muted color; `cursor: not-allowed`; `aria-disabled="true"` on custom elements
- **Loading** — spinner or skeleton; prevents re-submission; button width must not change (reserve space for spinner)
- **Error** — red border + icon + message; `aria-invalid="true"`; `role="alert"` on message element

**Motion requirements per state:**
- Hover transition: 150ms ease-out
- Active/press: 80ms ease-in (compress), 200ms ease-out (spring back)
- Loading entrance: fade-in + spinner animation (700–900ms rotation)
- Error: shake animation 300ms, or border color transition 150ms
- All states: `@media (prefers-reduced-motion: reduce)` override required

## Anatomy by component type

**Button:** Label, Container, [Icon left/right], [Loading spinner slot]
**Input:** Label, Container, Placeholder, [Icon left/right], Helper text, Error message, [Character count]
**Card:** Container, [Header/image], Body, [Footer/actions], [Badge/tag]
**Modal:** Overlay, Dialog, Header, Body, Footer, Close button; entrance: 200–300ms ease-out scale+fade from center
**Dropdown:** Trigger, Menu, Items, [Search], [Empty state]; open: 150ms ease-out, close: 100ms ease-in
**Toast:** Container, Icon, Title, [Description], [Action], [Close]; auto-dismiss: 3–4s; slide-in 200ms
**Badge:** Container, Label, [Icon/dot]
**Tabs:** Tab list, Active indicator, Panel; indicator transition: 200ms ease-out
**Accordion:** Trigger, Panel (height transition: 250ms ease-in-out), Chevron rotation

## Variant requirements

**Button:** Primary (filled accent), Secondary (outlined), Ghost/text, Destructive (red), [Icon-only]
**Input:** Default, Error, Success, Disabled, With icon(s), Multiline/textarea
**Badge:** Default, Success, Warning, Error, Info
**Card:** Default, Interactive/clickable (hover lift: shadow transition 150ms), Featured/highlighted

## Missing state templates — copy-paste JSX

### Loading state (Button)

```jsx
import { useRef, useState } from 'react';

const Spinner = ({ size = 16 }) => (
  <svg width={size} height={size} viewBox="0 0 16 16"
    style={{ animation: 'spin 800ms linear infinite', display: 'block' }}>
    <circle cx="8" cy="8" r="6" fill="none" stroke="currentColor"
      strokeWidth="2" strokeDasharray="28" strokeDashoffset="10" strokeLinecap="round"/>
    <style>{`
      @keyframes spin { to { transform: rotate(360deg); } }
      @media (prefers-reduced-motion: reduce) { svg { animation: none !important; } }
    `}</style>
  </svg>
);

// Add to your Button component:
const Button = ({ children, isLoading, disabled, onClick, ...props }) => {
  const ref = useRef(null);
  const [minWidth, setMinWidth] = useState('auto');

  const handleClick = () => {
    if (!minWidth || minWidth === 'auto') {
      setMinWidth(ref.current?.offsetWidth + 'px');
    }
    onClick?.();
  };

  return (
    <button
      ref={ref}
      style={{ minWidth }}
      disabled={isLoading || disabled}
      aria-busy={isLoading}
      onClick={handleClick}
      {...props}
    >
      {isLoading ? <Spinner size={16} /> : children}
    </button>
  );
};
```

### Error state (Input)

```jsx
const Input = ({ label, error, id, ...props }) => (
  <div>
    <label htmlFor={id}>{label}</label>
    <div style={{ position: 'relative' }}>
      <input
        id={id}
        aria-invalid={!!error}
        aria-describedby={error ? `${id}-error` : undefined}
        style={{ borderColor: error ? 'var(--color-error)' : undefined }}
        {...props}
      />
    </div>
    {error && (
      <p id={`${id}-error`} role="alert" style={{ color: 'var(--color-error)', fontSize: 'var(--text-sm)', marginTop: '4px' }}>
        {error}
      </p>
    )}
  </div>
);
```

### Empty state (List/Table)

```jsx
const EmptyState = ({ icon, title, description, action }) => (
  <div style={{ textAlign: 'center', padding: 'var(--spacing-16) var(--spacing-8)' }}>
    {icon && <div style={{ fontSize: '48px', marginBottom: 'var(--spacing-4)' }}>{icon}</div>}
    <h3 style={{ marginBottom: 'var(--spacing-2)' }}>{title}</h3>
    {description && <p style={{ color: 'var(--color-ink-500)', marginBottom: 'var(--spacing-6)' }}>{description}</p>}
    {action}
  </div>
);
```

### Disabled state (any interactive element)

```jsx
// For HTML buttons — use disabled attribute (browser handles aria)
<button disabled>Disabled</button>

// For custom interactive elements — must add aria-disabled manually
<div
  role="button"
  tabIndex={disabled ? -1 : 0}
  aria-disabled={disabled}
  style={{ opacity: disabled ? 0.45 : 1, cursor: disabled ? 'not-allowed' : 'pointer' }}
  onClick={disabled ? undefined : onClick}
  onKeyDown={disabled ? undefined : handleKeyDown}
>
  {label}
</div>
```

## Accessibility checklist

- All interactive elements keyboard-reachable (Tab key, logical order)
- `aria-label` on icon-only buttons
- `aria-disabled="true"` on custom disabled elements
- `aria-invalid="true"` on invalid inputs
- `role="alert"` or `aria-live="assertive"` on dynamically shown errors
- `aria-live="polite"` on success messages and loading status
- Color is NOT the only state indicator (error: icon + color + message, not just red border)
- Touch targets ≥ 24×24px (WCAG 2.5.8 minimum), recommended 44×44px
- Keyboard: Enter AND Space both work on button elements

## Output format

```
**[Component Name] Review**

States:
  ✓/✗ [State] — [issue if ✗]
  [If ✗ — paste the JSX template from "Missing state templates" above]

Variants:
  ✓/✗ [Variant] — [issue if ✗]

Motion:
  ✓/✗ [Motion finding]
  ```css
  /* Fix */
  [specific CSS with exact values]
  ```

Anatomy:
  ✓/✗ [Element] — [issue if ✗]

Accessibility:
  ✓/✗ [Finding] — [specific fix with exact attribute/value]

Priority fixes:
  1. [Highest impact] → radii.cloud/terms/[slug]
  2. ...
```

## Rules

- Be specific: not "add hover state" but "add `transition: background-color 150ms ease-out` and `background: var(--color-accent-600)` on hover"
- For every missing state, paste the copy-paste JSX from the templates above
- Motion findings count as much as visual findings — missing prefers-reduced-motion is a Critical issue

## Learn more

https://radii.cloud/terms/button-design
https://radii.cloud/terms/focus-visible
https://radii.cloud/terms/disabled-state
https://radii.cloud/terms/loading-state
https://radii.cloud/terms/error-state
https://radii.cloud/terms/animation
https://radii.cloud/terms/micro-interactions
