Progressive Complexity: When Islands Should be a Continent

Level 4 excels for isolated rich components, but heavy coordination signals when to intentionally escalate to a unified framework.


Progressive Complexity: When Islands Should be a Continent

As the Progressive Complexity Manifesto declares, we must “climb with intention, not habit.” This post expands on its deep dive into the Level 4-to-5 transition, showing when isolated islands should become a unified 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 (SolidStart 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.
  • When client-side routing is needed, so is a Level 5 solution
  • 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 many 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 Components (for example Lit)Level 3 enhanced with Web Components for complex, reusable interactive elements while maintaining server-first architecture.
5: Full FrameworkUnified client-side framework (e.g., SolidStart or NextJS) 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 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

New requirements arrive. Add‑to‑cart must update the header count, open a mini‑cart, and refresh recommendations. Login should update the header, personalize content, and show order history. Inventory changes must update availability, pricing, and related products.

Now multiple islands depend on the same client state. Nanostores is the natural choice in Astro to share that state. It’s tiny, ergonomic, and works across islands. The friction appears not in the store, but in the multiplication of subscriptions you must wire and maintain as coordination grows.

Nanostores wins, but subscriptions multiply

// stores/cart.ts
import { map, atom, computed } from "nanostores";

export const cartItems = map<
  Record<number, { id: number; price: number; qty: number }>
>({});
export const cartOpen = atom(false);

export const cartCount = computed(cartItems, (items) =>
  Object.values(items).reduce((s, i) => s + i.qty, 0)
);

export const cartTotal = computed(cartItems, (items) =>
  Object.values(items).reduce((s, i) => s + i.price * i.qty, 0)
);

Three separate islands now subscribe to shared state.

<!-- CartHeader.svelte (island) -->
<script>
  import { cartCount, cartTotal, cartOpen } from '../stores/cart'
  let count = $state(0)
  let total = $state(0)
  let open = $state(false)
  $effect(() => {
    const u1 = cartCount.subscribe(v => count = v)
    const u2 = cartTotal.subscribe(v => total = v)
    const u3 = cartOpen.subscribe(v => open = v)
    return () => { u1(); u2(); u3(); }
  })
</script>
<!-- MiniCart.svelte (island) -->
<script>
  import { cartItems, cartOpen } from '../stores/cart'
  let items = $state({})
  let open = $state(false)
  $effect(() => {
    const u1 = cartItems.subscribe(v => items = v)
    const u2 = cartOpen.subscribe(v => open = v)
    return () => { u1(); u2(); }
  })
</script>
<!-- Recommendations.svelte (island) -->
<script>
  import { cartItems } from '../stores/cart'
  let seed = $state('')
  $effect(() => {
    const u = cartItems.subscribe(v => { seed = JSON.stringify(v) })
    return () => u()
  })
</script>

This starts small, but grows fast. With 5 islands each subscribing to 3 stores, you maintain 15 subscriptions and 15 teardowns. Add wishlist, promotions, or free‑shipping thresholds and the graph expands again. Hydration order and partial remounts add timing edges you must reason about.

Example: adding a free‑shipping threshold touched 6 files across 4 islands; after consolidating into a single large island or unified app store, the same change touched 2 files.

Symptoms of subscription creep

  • Repeated subscribe/unsubscribe blocks across islands
  • Proliferation of derived stores to keep computed values in sync
  • Hydration‑order flicker and racey UI toggles
  • Small business changes trigger edits in many files

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. When the subscription graph keeps growing, consider one large island for localized coordination or a unified app model for app‑wide reactivity.

When Level 4 Works: The Sweet Spots

Level 4 excels for isolated rich components, as shown in the Progressive Complexity demo where table cells manage their own editing state without coordination. Embrace it for:

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.

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.

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.

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: One of Many Frameworks for Coordination

When coordination becomes the primary challenge, a fullstack framework like 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 much of the framework overhead, so the majority of the code is 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.

I argue that 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.

SolidStart and Next.js similarly reduce subscription boilerplate once you adopt a unified app model; the point is the cohesion, not the specific framework.

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

Decide with a Checklist

Using the manifesto’s spectrum as a guide:

  • 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 a fullstack framework like SvelteKit or SolidStart. Tasks, boards, assignees, and notifications all require rich client state with extensive reactivity across components.

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

Migration Guidance: Large Island vs SvelteKit/SolidStart/NextJS

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 a framework 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/SolidStart/NextJS offer 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. As the manifesto urges: Start simple. Scale intentionally. Revolution through restraint.