Progressive Complexity: When Islands Should be a Continent

Level 4 works perfectly for isolated rich components, but coordination between islands reveals when you've outgrown the Progressive Complexity approach.


Progressive Complexity: When Islands Should be a Continent

You’ve built an e-commerce site using Astro’s islands architecture. Beautiful product gallery, sophisticated search and filtering, rich review components. Level 4 is working perfectly. Then your product manager arrives with new requirements.

“We need shopping cart functionality that updates the header count, shows mini-cart previews, and triggers personalized recommendations.”

Suddenly, your perfectly isolated islands need to coordinate. Welcome to the Level 4 trap.

Progressive Complexity gives us a spectrum from static HTML to full frameworks, but there’s a critical decision point at Level 4 that many developers miss. Understanding when islands should become a continent can save you from building a framework in userland.

TL;DR

  • If 3+ islands must update together from shared client state, prefer a unified app model (SvelteKit or a single large island). Keep Level 4 for truly isolated components.
  • Start server-first. Add islands for local interaction. When reactivity spans multiple components, move to a unified app.
  • Before jumping to Level 5, consider hybrids: one large island/app shell, HTMX hx-swap-oob, SSE, or a small event bus. Use them until they strain under coordination.

Quick Recap: The Five Levels

The Progressive Complexity manifesto defines five levels from static HTML to full-stack frameworks. Here’s a brief overview of each:

LevelDescription
1: Static HTMLPure server-generated HTML with no client-side JavaScript. The foundation for all web applications.
2: HTML + HTMXServer-rendered HTML enhanced with HTMX for dynamic interactions without full page reloads. The sweet spot for most applications.
3: HTMX + Vanilla JSLevel 2 enhanced with vanilla JavaScript for client-side polish, validation, and UX improvements that HTMX alone cannot provide.
4: HTMX + Web ComponentsLevel 3 enhanced with Web Components for complex, reusable interactive elements while maintaining server-first architecture.
5: Full FrameworkUnified client-side framework (e.g., SvelteKit) for complex reactivity, shared state, and client-side routing when a cohesive app model is needed.

Most teams thrive at Levels 2–3 with server-managed state, Level 4 adds isolated islands, and Level 5 unifies reactivity across the app. When those islands need to coordinate, you’ve hit the trap.

The Interactivity vs Reactivity Line

Before diving into islands coordination, we need to distinguish between two fundamentally different types of user interface needs.

Interactivity means users can click buttons, fill out forms, and trigger actions. Think: clicking a “Sort by Price” button that refreshes the product list, submitting a contact form that shows a success message, or opening a date picker that updates a single field. Each interaction is self-contained. HTMX handles these beautifully with simple attributes like hx-get and hx-post.

Reactivity means data updates ripple automatically across multiple components. Think: adding an item to your cart and watching the header count badge, mini-cart preview, and recommendation engine all update simultaneously. Or changing your profile status and seeing the header, sidebar notifications, and dashboard widgets all react to that single change.

This distinction matters because it determines your architectural path. Interactivity without reactivity thrives at Levels 2-4. Reactivity with significant client state demands Level 5.

The key insight: interactivity without reactivity thrives at Levels 2-4 with server-first state management, while reactivity with significant client state demands Level 5’s unified approach.

The E-commerce Evolution Story

Let’s trace how a typical e-commerce site evolves from Level 4 success to Level 4 breakdown.

Phase 1: Islands Working Perfectly

Your initial requirements are straightforward. You need rich product image galleries, advanced search and filtering, and customer reviews with ratings. Each component is genuinely isolated. The image gallery manages its own slideshow state. The search component handles filtering logic internally. The review system operates independently.

Level 4 shines here. These islands have no reason to communicate. Each provides a focused, rich experience without coordination complexity. Your bundle size remains minimal, and so does the complexity of your codebase.

<!-- Each island operates independently -->
<ProductGallery product={product} client:load />
<SearchFilter client:load />
<ReviewSection productId={product.id} client:load />

Phase 2: Business Requirements Evolve

Then new requirements arrive. Adding items to cart must update the cart icon, show mini-cart previews, and trigger recommendation updates. User login should update the header, personalize content, and display order history. Inventory changes need to update product availability, pricing, and related product suggestions.

Now you have 5+ islands that need coordinated state changes. The isolation that made Level 4 elegant has become a coordination nightmare.

Note: carts can remain server-first using HTMX out-of-band swaps to update multiple regions. If that meets your coordination needs, you may not need Level 5.

Here’s a concrete example of a ProductCard component that needs to update the cart:

// ProductCard.svelte (Astro with islands)
<script>
import { $cartItems, addToCart } from '../stores/cart';

let { product } = $props();

// Even for a simple "Add to Cart" button, we need subscription logic
let inCart = $state(false);

$effect(() => {
  const unsubscribe = $cartItems.subscribe((items) => {
    inCart = !!items[product.id];
  });

  return () => unsubscribe();
});

function handleAddToCart() {
  addToCart({
    id: product.id,
    name: product.name,
    price: product.price,
    quantity: 1
  });

  // Maybe trigger a toast? That's another island to coordinate...
  // Maybe update recommendations? Another island...
  // Maybe show the mini-cart? More coordination...
}
</script>

<div class="product-card">
  <h3>{product.name}</h3>
  <p>${product.price}</p>
  <button onclick={handleAddToCart} disabled={inCart}>
    {inCart ? 'In Cart' : 'Add to Cart'}
  </button>
</div>

Compare this to the SvelteKit version:

// ProductCard.svelte (SvelteKit)
<script>
import { cartStore } from '$lib/stores/cart.svelte';

let { product } = $props();

// Direct reactive access - no subscriptions
let inCart = $derived(!!cartStore.items[product.id]);
</script>

<div class="product-card">
  <h3>{product.name}</h3>
  <p>${product.price}</p>
  <button
    onclick={() => cartStore.addToCart(product)}
    disabled={inCart}
  >
    {inCart ? 'In Cart' : 'Add to Cart'}
  </button>
</div>

The SvelteKit version is cleaner, more maintainable, and the reactivity propagates automatically to all components that need it.

To be fair, you can use Svelte 5 runes and similar store ergonomics inside Svelte islands in Astro. The friction appears when many separately hydrated islands must coordinate; a unified app context removes that overhead. In Astro, a viable hybrid is a single large island/app shell for the coordinated area, while keeping the rest server-first.

But this raises the question: when should you embrace Level 4’s isolation instead of fighting it?

When Level 4 Works: The Sweet Spots

Despite the coordination challenges, Level 4 excels for specific use cases that we should recognize and embrace.

Isolated rich components like date pickers, rich text editors, table headers with search and sort functionality, or data visualizations work beautifully as islands. A date picker doesn’t need to communicate with other components. It manages its calendar state, handles user selections, and reports the final value. Perfect island behavior.

Progressive enhancement scenarios where you’re adding one interactive element to mostly static pages are ideal for Level 4. Drop a rich comment editor into an otherwise static blog post. Add an interactive chart to a static report. These components enhance the page without requiring coordination.

Server-first development should be every frontend developer’s default approach. Most state belongs on the server, and Level 4 islands let you add rich interactions without abandoning that principle. But when coordination between components becomes essential, when client state becomes unavoidable, that’s the signal to embrace a different architecture rather than fight against it.

The Progressive Complexity demo shows this perfectly. Isolated interactive table cells work elegantly at Level 4 because each cell manages its own editing state without needing to coordinate with other cells.

Spot the Coordination Signals and State Anatomy

How do you recognize when Level 4 is breaking down? Several patterns indicate you’ve outgrown the islands approach, often tied to the type of state and reactivity your app requires.

As the Progressive Complexity manifesto reminds us: “Each abstraction should simplify something; when it doesn’t, we’ve abstracted too soon.” Islands are an abstraction meant to simplify component isolation, but when coordination becomes the primary challenge, this abstraction creates more problems than it solves.

Astro’s Recommended Solution: Nanostores

Astro recommends Nanostores for sharing state between islands. It’s a tiny (286 bytes), framework-agnostic state management library that works across React, Vue, Svelte, and vanilla JavaScript. Nanostores provides atom stores for simple values, map stores for objects, and computed stores for derived data.

The appeal is obvious: one small library that works everywhere. But when multiple islands need coordination, even this elegant solution reveals the architectural friction.

Multiple islands sharing state transforms Nanostores into Redux-lite. When you find yourself creating complex store structures with actions, reducers, and subscription management, you’re building framework features in userland.

Islands needing shared logic beyond just data creates architectural friction. When components need to coordinate workflows, state machines, or complex business rules, the isolation becomes counterproductive.

Building custom event systems for cross-island communication means you’re fighting the architecture. If you’re implementing pub-sub patterns, custom event buses, or complex message passing between islands, a unified framework would be simpler.

Client-side routing across a significant app surface is a strong signal you’re in Level 5 territory.

Here lies “the most dangerous moment” that the manifesto warns about: “The most dangerous moment comes with the first success of a complex solution, for it justifies all future complexity.” Getting one island coordination pattern working feels like validation, but it’s actually the moment to step back and consider whether Level 5 would be simpler.

For example, a header reacting to cart changes often repeats subscription boilerplate across islands:

<script>
import { $cartCount, $cartTotal, $cartOpen } from '../stores/cart';

let count = $state(0);
let total = $state(0);
let isOpen = $state(false);

$effect(() => {
  const unsubscribeCount = $cartCount.subscribe((value) => { count = value; });
  const unsubscribeTotal = $cartTotal.subscribe((value) => { total = value; });
  const unsubscribeOpen = $cartOpen.subscribe((value) => { isOpen = value; });
  return () => { unsubscribeCount(); unsubscribeTotal(); unsubscribeOpen(); };
});

function toggleCart() {
  $cartOpen.set(!$cartOpen.get());
}
</script>

<div class="cart-header">
  <button onclick={toggleCart}>
    Cart ({count}) - ${total.toFixed(2)}
  </button>
</div>

Every single component that touches shared state can end up repeating this subscription boilerplate. The complexity isn’t in your bundle size; it’s in your codebase maintenance burden.

Avoiding framework complexity can backfire if you end up recreating one.

Interactivity without reactivity handles isolated user actions perfectly. Each action triggers a server response with updated HTML. No automatic propagation occurs. Form submissions, search queries, and pagination requests fit this pattern beautifully at Levels 2-4. Optimistic updates are even possible with libraries like hx-optimistic.

Light reactivity can still be managed without frameworks. Custom events handle simple coordination. Server-sent events provide real-time updates. URL state changes can drive some reactive behaviors. HTMX’s out-of-band swapping lets the server update multiple page areas from a single request. The Progressive Complexity demo shows this beautifully with table cell editing that updates totals across the page using HTMX’s hx-swap-oob attribute.

Heavy reactivity demands framework support. Complex state synchronization across many components, state machines driving workflows, undo/redo functionality, and real-time collaboration features all require the infrastructure that frameworks provide.

SvelteKit: The Right Framework for Coordination

When coordination becomes the primary challenge, SvelteKit offers a cleaner solution than fighting with islands.

SvelteKit maintains clean separation between server and client concerns. Files ending in +page.server.js handle server logic, while +page.svelte components manage client interactions. This separation is clearer than mixing Astro’s static rendering with hydrated islands.

Svelte compiles away framework overhead, shipping only the code you write. Baseline bundles typically range from 40-60kb. Built-in stores handle client state management without external dependencies. Forms work without JavaScript through progressive enhancement, maintaining the server-first philosophy.

React-based metaframeworks like Next.js and TanStack Start add mental overhead with Server Components and “use client” boundary decisions, while shipping larger bundles than Svelte’s compiled output. However, Next.js’s RSC can meaningfully reduce client JS for many patterns; the tradeoffs are in mental model and boundary decisions. SvelteKit avoids much of this complexity with its clear server-client separation.

Compare the island boilerplate above with SvelteKit’s approach. First, the store definition using Svelte 5 runes:

// $lib/stores/cart.svelte.ts
interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

class CartStore {
  // Private reactive state using $state rune (Svelte 5's way to create reactive variables)
  private items = $state<Record<number, CartItem>>({});
  isOpen = $state(false); // Another $state rune for reactive boolean

  // Computed values are just getters - no manual computation needed ($derived would make this automatically reactive if needed)
  get cartCount() {
    return Object.values(this.items).reduce(
      (sum, item) => sum + item.quantity,
      0
    );
  }

  get cartTotal() {
    return Object.values(this.items).reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  }

  // Methods to modify state
  addToCart(item: CartItem) {
    this.items[item.id] = item;
  }

  removeFromCart(id: number) {
    delete this.items[id];
  }

  toggleCart() {
    this.isOpen = !this.isOpen;
  }
}

// Export a single instance
export const cartStore = new CartStore();

And here’s how clean the component becomes:

<!-- CartHeader.svelte - Zero boilerplate -->
<script>
import { cartStore } from '$lib/stores/cart.svelte';
</script>

<div class="cart-header">
  <button onclick={() => cartStore.toggleCart()}>
    Cart ({cartStore.cartCount}) - ${cartStore.cartTotal.toFixed(2)}
  </button>
</div>

No subscriptions. No cleanup. No manual state synchronization. The reactivity just works.

The cleverness of avoiding a framework is outdone only by the foolishness of reinventing one.

Decide with a Checklist

  • Choose Level 4 if updates are local, no meaningful shared client state, no client-side routing, and the server remains authoritative.
  • Choose Level 5 if multiple components require cross-component reactivity, you maintain significant client state, you need client routing, real-time/offline, or complex workflows.

The State-Based Decision Matrix

Different application types naturally fit different levels based on their state management needs. Here’s a flowchart to guide your decision:

State Decision Flowchart

An e-commerce site typically works well at Levels 2-3. Server state manages cart contents, product catalogs, and user sessions. Individual components can handle their interactions independently.

A blog with comments fits Levels 2-3 perfectly. Content management, user authentication, and comment submission all work through server state with minimal client coordination needs.

A dashboard with charts might use Levels 3-4. Individual charts can operate as isolated interactive components, each managing their own visualization state without needing to coordinate with other dashboard elements.

A project management application demands Level 5 with SvelteKit. Tasks, boards, assignees, and notifications all require rich client state with extensive reactivity across components.

A collaborative editor definitely needs Level 5 with SvelteKit. Real-time editing, user presence, conflict resolution, and document state all require complex client state management.

Migration Guidance: Large Island vs SvelteKit

When Level 4’s isolation breaks down, you have three architectural paths forward. Choose based on coordination scope and complexity.

Try a Large Island First if coordination is localized to a specific app area. Instead of multiple small islands that need to communicate, create one larger island that encompasses the coordinated functionality. For example, consolidate your product gallery, add-to-cart button, and mini-cart preview into a single “ProductArea” island. This maintains the server-first approach while solving the coordination problem.

Consider Hybrid Solutions before jumping to Level 5. HTMX’s out-of-band swaps can update multiple page regions from a single server response, maintaining server authority while providing coordinated updates. Server-sent events handle real-time coordination without client state complexity. These patterns work well when the server can manage the coordination logic.

Move to SvelteKit when coordination becomes the dominant architectural concern. If you need state synchronization across the header, sidebar, main content, modals, and navigation, essentially the entire application surface, a unified app model reduces complexity rather than adding it. Client-side routing across multiple pages also signals it’s time for Level 5.

The key insight: scope determines architecture. Localized coordination can stay at Level 4 with larger islands. Application-wide coordination needs Level 5’s unified model.

State Determines Architecture

The fundamental insight is that your state requirements should guide architectural decisions, not bundle size metrics or technology preferences.

A common confusion in our framework-heavy era is assuming state needs to be client-side by default. Most application state actually belongs on the server: user authentication, shopping carts, form data, search filters, and business logic. The prevalence of frontend frameworks has created a generation of developers who instinctively reach for client-side state management when server-side state would be simpler, more reliable, and more performant.

When client state becomes primary to your application’s functionality, frameworks reduce complexity rather than add it. SvelteKit offers the best of both worlds: server-first development with rich client capabilities when needed.

Don’t fight to keep state on the server when it naturally belongs on the client. Recognize when your islands need to become a continent. The complexity isn’t in choosing a framework; it’s in choosing the wrong abstraction level for your coordination requirements.

Let your state requirements guide your architecture choice. Progressive Complexity gives you the spectrum. Your application’s coordination needs determine where you should land.