SuperIslandSuperIsland
Extension SDK

Extension Lifecycle

How registerModule works — render functions, lifecycle hooks, and action handlers.

Extension Lifecycle

Every extension calls DynamicIsland.registerModule() with an object describing how to render and respond to events.

registerModule

DynamicIsland.registerModule({
  // Render functions (at least compact is required)
  compact(): ViewNode,
  expanded(): ViewNode,
  fullExpanded(): ViewNode,

  // Minimal compact (leading/trailing indicators)
  minimalCompact: {
    leading(): ViewNode,
    trailing(): ViewNode,
  },

  // Lifecycle hooks
  onActivate(): void,
  onDeactivate(): void,

  // Interaction handler
  onAction(actionID: string, value?: unknown): void,
})

Render functions

compact() — required

Called on every refreshInterval tick. Return a ViewNode describing the compact island content.

compact() {
  const time = new Date().toLocaleTimeString();
  return View.text(time, { style: "caption", color: "#e8e8e8" });
}

Called when the user hovers or taps the island. Return a richer view.

expanded() {
  return View.vstack([
    View.text("My Extension", { style: "headline" }),
    View.text("Extended content here", { color: "#888" }),
  ], { spacing: 6 });
}

fullExpanded() — optional

Called for the full expanded state. Falls back to expanded() if not provided.

minimalCompact — optional

For leading/trailing pill indicators (like iOS's camera/mic indicators).

minimalCompact: {
  leading() {
    return View.icon("circle.fill", { color: "#22c55e", size: 8 });
  },
  trailing() {
    return View.text("On", { style: "caption2", color: "#22c55e" });
  },
}

Lifecycle hooks

onActivate()

Called when the extension becomes active (island starts showing this extension).

onActivate() {
  console.log("Extension activated");
  DynamicIsland.island.activate();
}

onDeactivate()

Called when the extension is deactivated.

onDeactivate() {
  // Cleanup, stop timers, etc.
}

Action handler

onAction(actionID, value)

Called when an interactive view element fires. The actionID corresponds to the string you passed to View.button(), View.toggle(), or View.slider().

onAction(actionID, value) {
  if (actionID === "toggle-timer") {
    const running = DynamicIsland.store.get("running");
    DynamicIsland.store.set("running", !running);
  }

  if (actionID === "set-volume") {
    console.log("Volume set to:", value);
  }
}

Full example

let seconds = 0;
let running = false;

DynamicIsland.registerModule({
  compact() {
    const mins = Math.floor(seconds / 60).toString().padStart(2, "0");
    const secs = (seconds % 60).toString().padStart(2, "0");
    return View.hstack([
      View.icon("timer", { size: 12, color: "#a855f7" }),
      View.text(`${mins}:${secs}`, { style: "caption", color: "#e8e8e8" }),
    ], { spacing: 4 });
  },

  expanded() {
    return View.vstack([
      View.timerText(seconds, { style: "headline" }),
      View.hstack([
        View.button(View.text(running ? "Pause" : "Start"), "toggle"),
        View.button(View.text("Reset"), "reset"),
      ], { spacing: 8 }),
    ], { spacing: 8 });
  },

  onActivate() {
    running = DynamicIsland.store.get("running") ?? false;
    seconds = DynamicIsland.store.get("seconds") ?? 0;
  },

  onAction(id) {
    if (id === "toggle") {
      running = !running;
      DynamicIsland.store.set("running", running);
    }
    if (id === "reset") {
      seconds = 0;
      running = false;
    }
  },
});

setInterval(() => {
  if (running) {
    seconds++;
    DynamicIsland.store.set("seconds", seconds);
  }
}, 1000);

On this page