Mischief

125 / Feedback

Status Island

A small pill at the edge of the screen that reports background work. It reshapes as tasks come and go, opens into the full list, and leaves once everything is done.

Start some work, then open the island to see every task.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/status-island
import { StatusIsland } from "mischief-ui/status-island"

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/status-island/status-island.tsx
"use client" import * as React from "react" import { cn } from "@/lib/utils" export type StatusIslandTaskStatus = "running" | "done" | "error" export interface StatusIslandTask {  id: string  label: string  /** From 0 to 1. Leave it out while the size of the work is unknown. */  progress?: number  status: StatusIslandTaskStatus

Usage

export function Uploads() {
  const { tasks, cancel, retry } = useUploads()

  return (
    <StatusIsland
      tasks={tasks.map((upload) => ({
        id: upload.id,
        label: upload.file.name,
        progress: upload.loaded / upload.total,
        status: upload.failed ? "error" : upload.done ? "done" : "running",
      }))}
      onCancel={cancel}
      onRetry={retry}
    />
  )
}

What it reads

One array. Each task has an id, a label, a status of running, done or error, and an optional progress from 0 to 1. Leave progress out while the size of the work is unknown and the ring spins instead of filling. detail is a second line, usually the reason a task failed.

The island keeps no copy of the work. Cancel and retry hand back the id, and the host changes the array.

Which task leads

A failure comes first, because it is the one that needs a person. Otherwise the first running task leads, with its percent, and a count says how many others are still open. When nothing is running and nothing failed the island says Done, waits doneDuration, and leaves. It stays while it is open or holds focus, so it never disappears under a keyboard.

Placement

It is fixed at the top center by default. Pass a className to move it, such as bottom-4 top-auto, or absolute to keep it inside a container.

API

tasksStatusIslandTask[]The work to report: { id, label, status, progress?, detail? }.
onCancel(id: string) => voidCancels a running task. Omit it and no Cancel button is drawn.
onRetry(id: string) => voidRetries a failed task. Omit it and no Retry button is drawn.
labelstringThe accessible name of the island. Defaults to Background tasks.
doneLabel, failedLabel, cancelLabel, retryLabelstringStatus words and button copy.
doneDurationnumberHow long Done stays before the island leaves, in milliseconds. Defaults to 1600.
status (StatusIslandTaskStatus)"running" | "done" | "error"A task's status.
data-state (StatusIslandState)"hidden" | "running" | "error" | "done"What the island is showing, written to the root for styling. data-expanded is set while the list is open.
classNamestringClasses for placement.
...divPropsHTMLAttributesNative div attributes.

Accessibility

The island is a named region with one button that says what leads and whether the list is expanded. Enter or a click opens the list, and Escape or a click outside closes it and returns focus to that button. Each running task has a named progress bar, and Cancel and Retry name the task they act on. Completions and failures are announced politely from a live region kept outside the island, so they are read even as it leaves. Progress ticks are not announced. While hidden the island is inert. Reduced motion removes the reshaping, the ring's spin and the bar transitions.