DynamicIsland

Square status island: it springs to the size of its content and blurs new content in whenever that content changes.

Usage

svelte
<script lang="ts">
  import { Timer } from "@lucide/svelte";
  import { DynamicIsland } from "@milandekruijf/ui/svelte";

  let active = $state(false);
</script>

<DynamicIsland
  label={active ? "Stop timer" : "Start timer"}
  onclick={() => (active = !active)}
>
  <Timer />
  {#if active}0:10{/if}
</DynamicIsland>

Examples

Focus timer

Click the island to start, pause, and reset.

<script lang="ts">
  import { onDestroy } from "svelte";
  import { css, cx } from "@milandekruijf/pandacss/css";
  import { AnimatedCharFlip, DynamicIsland } from "@milandekruijf/ui/svelte";
  import { Check, Pause, Timer } from "@lucide/svelte";

  type State = "idle" | "running" | "paused" | "done";

  /** Short session so the demo can actually finish. */
  const DURATION = 10;
  const RESET_DELAY = 2400;

  let phase = $state<State>("idle");
  let remaining = $state(DURATION);
  let ticker: ReturnType<typeof setInterval> | null = null;
  let resetTimer: ReturnType<typeof setTimeout> | null = null;

  function format(seconds: number): string {
    const m = Math.floor(seconds / 60);
    const s = seconds % 60;
    return `${m}:${String(s).padStart(2, "0")}`;
  }

  const time = $derived(format(remaining));

  const label = $derived(
    phase === "idle"
      ? "Start focus session"
      : phase === "running"
        ? "Pause focus session"
        : phase === "paused"
          ? "Resume focus session"
          : "Reset focus session",
  );

  function stopTicker() {
    if (ticker !== null) clearInterval(ticker);
    ticker = null;
  }

  function stopReset() {
    if (resetTimer !== null) clearTimeout(resetTimer);
    resetTimer = null;
  }

  function run() {
    stopTicker();
    ticker = setInterval(() => {
      remaining = Math.max(0, remaining - 1);
      if (remaining > 0) return;
      stopTicker();
      phase = "done";
      resetTimer = setTimeout(() => {
        resetTimer = null;
        phase = "idle";
        remaining = DURATION;
      }, RESET_DELAY);
    }, 1000);
  }

  function toggle() {
    if (phase === "idle") {
      remaining = DURATION;
      phase = "running";
      run();
    } else if (phase === "running") {
      stopTicker();
      phase = "paused";
    } else if (phase === "paused") {
      phase = "running";
      run();
    } else {
      stopReset();
      phase = "idle";
      remaining = DURATION;
    }
  }

  onDestroy(() => {
    stopTicker();
    stopReset();
  });

  const stageClass = css({
    display: "flex",
    flexDirection: "column",
    alignItems: "center",
    gap: 4,
    py: 6,
    w: "100%",
  });

  const iconClass = css({ w: 5, h: 5, flexShrink: 0 });

  const idleIconClass = css({ w: 4, h: 4, color: "slate.11" });

  const runningIconClass = css({ color: "blue.11" });

  const mutedIconClass = css({ color: "slate.11" });

  const doneIconClass = css({ color: "green.11" });

  const timeClass = css({
    display: "inline",
    whiteSpace: "nowrap",
    fontVariantNumeric: "tabular-nums",
    transition: "opacity 0.2s ease-in-out",
  });

  const dimClass = css({ opacity: 0.5 });

  const hintClass = css({
    fontSize: "sm",
    color: "slate.11",
    m: 0,
  });
</script>

<div class={stageClass}>
  <DynamicIsland {label} onclick={toggle}>
    {#if phase === "idle"}
      <Timer class={cx(iconClass, idleIconClass)} aria-hidden="true" />
    {:else if phase === "done"}
      <Check class={cx(iconClass, doneIconClass)} aria-hidden="true" />
      <span>Complete</span>
    {:else}
      {#if phase === "paused"}
        <Pause class={cx(iconClass, mutedIconClass)} aria-hidden="true" />
      {:else}
        <Timer class={cx(iconClass, runningIconClass)} aria-hidden="true" />
      {/if}
      <span class={cx(timeClass, phase === "paused" && dimClass)}>
        {#each [...time] as char, index (index)}
          {#if char >= "0" && char <= "9"}
            <AnimatedCharFlip {char} />
          {:else}
            <span>{char}</span>
          {/if}
        {/each}
      </span>
    {/if}
  </DynamicIsland>

  <p class={hintClass}>Click the island to start, pause, and reset.</p>
</div>

Props

PropTypeDefaultDescription
class?string—
Extra classes merged on the root.
label?string—
Accessible name; required once `onclick` makes the island a button.
onclick?(event: MouseEvent) => void—
When set, the island itself renders as a button, with the whole box as the hit area.
childrenSnippet—
Island content; its measured size drives the box. Keep it single-line.

The island measures its content and springs its box to the new size whenever the content changes, blurring the new side in; changes that keep the same box (e.g. a ticking digit) leave it still. The island already carries `px: 4`, `py: 2`, and its typography; the squared surface matches the site's buttons — `slate.3` behind `slate.12`, lifting to `slate.4`/`slate.5` on hover and press.