# Astro Islands

> - Adding interactive components to Astro pages

- Skill: `majiayu000/astro-islands` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/astro-islands`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/astro-islands/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/astro-islands

---


# Astro Islands Architecture Skill

## When to Use This Skill

Use this skill when:
- Adding interactive components to Astro pages
- Deciding between React, vanilla JS, or static HTML
- Troubleshooting hydration errors
- Optimizing JavaScript bundle size

## Core Principle

> **React is for composition and layout. Vanilla JS is for interactivity.**

In Astro, the best architecture ships minimal JavaScript:
- Use React/components for **SSR composition** (zero JS shipped)
- Use vanilla JS for **simple interactions** (theme toggle, mobile menu)
- Use `client:only` **sparingly** for complex widgets that truly need React state

## Astro Client Directives

| Directive | Server Renders? | Ships JS? | Use For |
|-----------|-----------------|-----------|---------|
| None | Yes | No | Static content, layouts |
| `client:load` | Yes | Yes | Hydrates immediately (avoid if possible) |
| `client:idle` | Yes | Yes | Hydrates when idle |
| `client:visible` | Yes | Yes | Hydrates when visible |
| `client:only="react"` | No | Yes | Complex widgets, no SSR |

## Architecture Patterns

### Pattern 1: Pure SSR (Recommended for most content)

```astro
---
// index.astro
import { HomePage } from '../components/HomePage';
const data = await fetchData();
---
<HomePage data={data} />  <!-- NO client directive = static HTML -->
```

**What you get:**
- React components render to static HTML at build time
- Zero JavaScript shipped
- Fully testable in isolation
- Fast page loads

### Pattern 2: Vanilla JS for Simple Interactivity

```astro
---
// Layout.astro
---
<button id="theme-toggle">Toggle Theme</button>

<script is:inline>
  document.getElementById('theme-toggle').addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme');
    const next = current === 'dark' ? 'light' : 'dark';
    document.documentElement.setAttribute('data-theme', next);
    localStorage.setItem('theme', next);
  });
</script>
```

**Use for:**
- Theme toggles (~20 lines)
- Mobile menu open/close (~15 lines)
- Simple form validation
- Scroll-triggered animations

**Benefits:**
- ~0.5kb vs ~5kb+ for React hydration
- No hydration bugs
- Works immediately (no waiting for React to load)

### Pattern 3: client:only for Complex Widgets (Last Resort)

```astro
---
// page.astro
import { PhotoGallery } from '../components/PhotoGallery';
---
<!-- Only when vanilla JS isn't sufficient -->
<PhotoGallery client:only="react" photos={photos} />
```

**Use for:**
- Complex state management
- Heavy user interaction (drag-drop, canvas)
- Third-party React libraries

**Trade-offs:**
- Ships React runtime (~5kb+)
- Renders empty until JS loads
- Cannot be nested inside SSR React

## Critical Constraint

### You CANNOT Nest Client Islands Inside SSR React

```astro
<!-- THIS DOES NOT WORK -->
<HomePage data={data} />  <!-- SSR React -->
<script>
  // Trying to mount React inside a div that HomePage rendered
  createRoot(document.getElementById('widget-root')).render(<Widget />);
</script>
```

**Error:** `@vitejs/plugin-react can't detect preamble`

**Why:** Astro's React integration doesn't support split-brain React:
- Part server-rendered
- Part client-mounted into server-rendered placeholders

**Solution:** Use vanilla JS for the interaction, or restructure so the interactive part is a sibling, not a child.

## Decision Tree

```
Need to render HTML?
├── Yes → Use React component (no directive)
│   └── Need interactivity?
│       ├── Simple (toggle, menu) → Vanilla JS script
│       └── Complex (gallery, forms) → Separate client:only component
└── No → Why are you here?
```

## Performance Comparison

| Approach | JS Shipped | Time to Interactive |
|----------|------------|---------------------|
| SSR React (no directive) | 0kb | Instant |
| SSR + Vanilla JS | ~0.5kb | Instant |
| SSR + client:load | ~5kb+ | After React loads |
| client:only | ~5kb+ | After React loads |

## Common Pitfalls

### DON'T: Use client:load on Layout Components
```astro
<!-- Bad: Ships React for static content -->
<Layout client:load>
  <Content />
</Layout>
```

### DON'T: Over-engineer Simple Interactions
```astro
<!-- Bad: React for a toggle -->
<ThemeToggle client:only="react" />

<!-- Good: Vanilla JS -->
<button id="theme-toggle">Toggle</button>
<script is:inline>/* 10 lines */</script>
```

### DON'T: Mix Astro Components Inside React
```tsx
// This DOES NOT WORK
export const Page = () => {
  return <AstroComponent />;  // Can't import .astro into .tsx
};
```

### DO: Keep React Containers Pure SSR
```tsx
// Good: No useState, no useEffect
export const HomePage: React.FC<Props> = ({ articles }) => {
  return (
    <>
      <Header />
      <ArticleGrid articles={articles} />
      <Footer />
    </>
  );
};
```

## Script Directive Reference

| Directive | Bundled? | Deferred? | Use For |
|-----------|----------|-----------|---------|
| `<script>` | Yes | Yes | Most scripts |
| `<script is:inline>` | No | No | Theme init (before paint), small scripts |

Use `is:inline` for:
- Theme initialization (must run before first paint)
- Tiny scripts where bundling overhead isn't worth it
- Scripts that need to run synchronously

## Testing Strategy

| What | Where | How |
|------|-------|-----|
| React components | Ladle/Storybook | Visual regression |
| Full page with interactions | Astro dev server | Playwright integration tests |
| Vanilla JS | Integration tests | Playwright |

You cannot test vanilla JS interactions in component isolation tools like Ladle - they need the full Astro page context.

