---
name: radii-loading
description: Generate or audit loading states — all 5 types (spinner, progress bar, skeleton, shimmer, inline) with production CSS/JSX and a decision tree
version: 1.0.0
author: radii.cloud
---

# Radii Loading — `/radii-loading`

> Generate or audit any loading state using Radii's 5-type loading system. Outputs production CSS + JSX with decision logic.

## What this skill does

You are a loading state specialist. When invoked:
- **Audit mode** — given existing code, identify the loader type, flag problems, output CSS fixes
- **Generate mode** — given a description or component context, pick the right type and output complete production CSS + JSX

## The 5 loading state types

### Type 1: Spinner (Indeterminate)

**Use when:** Duration unknown, < 3 seconds expected, small inline context (buttons, icons, inline actions)
**Avoid when:** User needs progress feedback, loading > 5 seconds, replacing large content areas

```css
@keyframes spin { to { transform: rotate(360deg); } }

.spinner {
  display: inline-block;
  width: 20px; height: 20px;
  border: 2px solid var(--color-ink-100);
  border-top-color: var(--color-accent-500);
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}

/* Size variants */
.spinner--sm { width: 14px; height: 14px; border-width: 2px; }
.spinner--lg { width: 32px; height: 32px; border-width: 3px; }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; opacity: 0.5; }
}
```

---

### Type 2: Progress Bar (Determinate)

**Use when:** Duration is known or calculable — file uploads, multi-step forms, downloads, onboarding flows
**Avoid when:** Progress can't be tracked accurately (fake progress bars destroy trust)

```css
.progress-track {
  height: 4px;
  background: var(--color-ink-100);
  border-radius: var(--radius-full, 9999px);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: var(--color-accent-500);
  border-radius: var(--radius-full, 9999px);
  transition: width var(--motion-base, 220ms) cubic-bezier(0.16, 1, 0.3, 1);
  /* Update via: element.style.width = progress + '%' */
}
```

```html
<div class="progress-track" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" aria-label="Uploading file">
  <div class="progress-fill" style="width: 40%"></div>
</div>
```

---

### Type 3: Skeleton Screen (Structural Placeholder)

**Use when:** Loading > 1 second, content has predictable structure, dashboard cards, feed lists, profile pages
**Avoid when:** Content is unpredictable, modal context, < 500ms loading (flash of skeleton is worse than nothing)

```css
.skeleton {
  background: linear-gradient(
    90deg,
    var(--color-ink-100) 25%,
    var(--color-ink-50) 50%,
    var(--color-ink-100) 75%
  );
  background-size: 200% 100%;
  animation: skeleton-wave 1.5s ease-in-out infinite;
  border-radius: var(--radius-sm, 8px);
}

@keyframes skeleton-wave {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Common skeleton shapes */
.skeleton-text { height: 16px; margin-bottom: 8px; border-radius: 4px; }
.skeleton-text--short { width: 40%; }
.skeleton-heading { height: 24px; width: 60%; margin-bottom: 16px; }
.skeleton-avatar { width: 40px; height: 40px; border-radius: 50%; }
.skeleton-card { height: 200px; border-radius: var(--radius-md, 12px); }

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
    background: var(--color-ink-100);
  }
}
```

---

### Type 4: Inline / Button Loader (Micro-interaction)

**Use when:** Form submit, button action, replace button text with spinner — any single-element async trigger
**Critical rule:** Always lock the button width before switching to spinner to prevent layout shift

```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' }}>
    <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); } }`}</style>
  </svg>
);

const LoadingButton = ({ label, isLoading, onClick, disabled, ...props }) => {
  const ref = useRef(null);
  const [minWidth, setMinWidth] = useState('auto');

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

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

---

### Type 5: Shimmer (Animated Skeleton Variant)

**Use when:** Premium feel needed — e-commerce product cards, media content, high-end dashboards
**Difference from skeleton:** Diagonal shine sweep instead of a left-to-right wave. More polished, slightly heavier.

```css
@keyframes shimmer {
  0% { background-position: -468px 0; }
  100% { background-position: 468px 0; }
}

.shimmer {
  background-color: var(--color-ink-100);
  background-image: linear-gradient(
    to right,
    var(--color-ink-100) 0%,
    var(--color-ink-50) 20%,
    var(--color-ink-100) 40%,
    var(--color-ink-100) 100%
  );
  background-size: 800px 100%;
  animation: shimmer 1.2s linear infinite forwards;
  border-radius: var(--radius-sm, 8px);
}

/* Dark mode shimmer */
@media (prefers-color-scheme: dark) {
  .shimmer {
    background-color: var(--color-ink-800);
    background-image: linear-gradient(
      to right,
      var(--color-ink-800) 0%,
      var(--color-ink-700) 20%,
      var(--color-ink-800) 40%,
      var(--color-ink-800) 100%
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  .shimmer { animation: none; }
}
```

---

## Decision tree

```
1. Do you know the exact progress percentage? 
   YES → Progress Bar (Type 2)

2. Is this triggered by a button click or single-element action?
   YES → Inline / Button Loader (Type 4)

3. Does the content have predictable layout structure?
   YES → Is it premium / e-commerce context?
     YES → Shimmer (Type 5)
     NO  → Skeleton Screen (Type 3)

4. Everything else (page load, API, async data)
   → Spinner (Type 1)
```

## Output format — audit mode

```
**Loading state audit**

Type detected: [Spinner / Progress Bar / Skeleton / Shimmer / Inline Loader]
Is this the right type? [Yes — correct for this context] OR [No — should be [Type] because [reason]]

Issues:
  [SEVERITY] [issue description]
  ```css
  /* Before */
  [current code]
  /* After */
  [fixed code]
  ```

Accessibility checklist:
  ✓/✗ aria-busy="true" on container during loading
  ✓/✗ aria-label="Loading…" on spinner (icon-only)
  ✓/✗ prefers-reduced-motion override
  ✓/✗ Width lock on button (no layout shift)
  ✓/✗ Prevents duplicate submissions (button disabled during load)
```

## Output format — generate mode

Output the complete CSS + JSX/HTML for the chosen type, ready to copy-paste. Include:
- The CSS class definitions
- An HTML/JSX usage example
- ARIA attributes
- The prefers-reduced-motion override
- Any JS logic required (for inline loaders)

## Links

- radii.cloud/terms/loading-state
- radii.cloud/terms/skeleton-loader
- radii.cloud/terms/micro-interactions
- radii.cloud/terms/animation
- radii.cloud/terms/optimistic-ui
