Skip to content
Dethink Components

ComponentsFeedback States

Feedback States

Show loading, progress, errors, empty states, and notifications.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  Alert,
  Announcer,
  Callout,
  EmptyState,
  LiveRegionProvider,
  Progress,
  ProgressCircle,
  Skeleton,
  SkeletonAvatar,
  SkeletonButton,
  SkeletonText,
  Spinner,
  ToastProvider,
  ToastViewport,
  useAnnouncer,
  useToast,
} from "@dethink/components";

Feedback primitives compose across loading, async status, empty data, and mutation confirmation flows.

Loading and progress

Spinner is decorative unless labelled. Progress exposes determinate or indeterminate progressbar semantics, while Skeleton reserves layout space.

Import progress68%
72%
Show sourceexamples/feedback-states/loading.tsx
examples/feedback-states/loading.tsx
"use client";

import {
  Progress,
  ProgressCircle,
  SkeletonAvatar,
  SkeletonButton,
  SkeletonText,
  Spinner,
} from "@dethink/components";

export function FeedbackLoading() {
  return (
    <div className="grid gap-6">
      <div className="flex flex-wrap items-center gap-4">
        <Spinner label="Loading reports" tone="primary" />
        <Spinner aria-label="Refreshing" variant="dots" tone="muted" />
        <Progress label="Import progress" value={68} showValue tone="info" />
      </div>
      <div className="border-border grid gap-4 rounded-lg border p-4">
        <div className="flex items-center gap-4">
          <ProgressCircle label="Upload" value={72} showValue tone="success" />
          <div className="grid flex-1 gap-2">
            <SkeletonText lines={3} animation="shimmer" />
            <div className="flex items-center gap-3">
              <SkeletonAvatar />
              <SkeletonButton />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Messaging and empty states

Alert carries status urgency, Callout stays contextual, and EmptyState keeps action slots predictable.

Deployment queued
The release will continue in the background.
Large imports
Files over 100 MB continue processing after this panel closes.
No failed jobs
Background jobs that need attention will appear here.
Show sourceexamples/feedback-states/messaging.tsx
examples/feedback-states/messaging.tsx
"use client";

import { AlertTriangle, CheckCircle2, Inbox, UploadCloud } from "lucide-react";
import { Alert, Button, Callout, EmptyState } from "@dethink/components";

export function FeedbackMessaging() {
  return (
    <div className="grid gap-4">
      <Alert
        icon={<CheckCircle2 />}
        title="Deployment queued"
        description="The release will continue in the background."
        tone="success"
      />
      <Alert
        icon={<AlertTriangle />}
        title="Payment retry failed"
        description="Update the payment method before retrying the invoice."
        tone="destructive"
        urgency="assertive"
      />
      <Callout
        icon={<UploadCloud />}
        title="Large imports"
        description="Files over 100 MB continue processing after this panel closes."
      />
      <EmptyState
        visual={<Inbox />}
        title="No failed jobs"
        description="Background jobs that need attention will appear here."
        primaryAction={<Button variant="outline">Refresh</Button>}
        variant="table"
      />
    </div>
  );
}

Shared announcements

LiveRegionProvider centralizes polite and assertive announcements and supports debounced result-count updates.

Polite
No result announcement sent.
Assertive
No failure announcement sent.
Show sourceexamples/feedback-states/announcer.tsx
examples/feedback-states/announcer.tsx
"use client";

import { useState } from "react";
import { Button, LiveRegionProvider, useAnnouncer } from "@dethink/components";

function AnnouncerControls() {
  const announcer = useAnnouncer();
  const [count, setCount] = useState(0);
  const [politePreview, setPolitePreview] = useState(
    "No result announcement sent.",
  );
  const [assertivePreview, setAssertivePreview] = useState(
    "No failure announcement sent.",
  );

  return (
    <div className="grid gap-4">
      <div className="flex flex-wrap gap-2">
        <Button
          variant="outline"
          onClick={() => {
            const next = count + 1;
            const message = `${next} filtered ${next === 1 ? "result" : "results"} available`;

            setCount(next);
            setPolitePreview(message);
            announcer.announcePolite(message, {
              coalesceKey: "results",
              debounceMs: 150,
            });
          }}
        >
          Announce results
        </Button>
        <Button
          variant="destructive"
          onClick={() => {
            const message = "Connection lost";

            setAssertivePreview(message);
            announcer.announceAssertive(message);
          }}
        >
          Announce failure
        </Button>
      </div>
      <dl className="grid gap-2 sm:grid-cols-2">
        <div className="border-border bg-background/80 rounded-md border p-3 text-sm shadow-sm">
          <dt className="text-muted-foreground text-xs font-semibold tracking-[0.14em] uppercase">
            Polite
          </dt>
          <dd className="text-foreground mt-1 font-medium">{politePreview}</dd>
        </div>
        <div className="border-border bg-background/80 rounded-md border p-3 text-sm shadow-sm">
          <dt className="text-muted-foreground text-xs font-semibold tracking-[0.14em] uppercase">
            Assertive
          </dt>
          <dd className="text-foreground mt-1 font-medium">
            {assertivePreview}
          </dd>
        </div>
      </dl>
    </div>
  );
}

export function FeedbackAnnouncer() {
  return (
    <LiveRegionProvider>
      <AnnouncerControls />
    </LiveRegionProvider>
  );
}

Toast workflow

Toast uses the shared announcer, explicit dismiss controls, auto-dismiss timers, pause-on-hover/focus, and Motion-powered stack presence.

    Show sourceexamples/feedback-states/toast-workflow.tsx
    examples/feedback-states/toast-workflow.tsx
    "use client";
    
    import { AlertTriangle, CheckCircle2, ListChecks } from "lucide-react";
    import {
      Button,
      ToastProvider,
      ToastViewport,
      useToast,
    } from "@dethink/components";
    
    function ToastControls() {
      const { toast } = useToast();
    
      return (
        <div className="flex flex-wrap gap-2">
          <Button
            leftIcon={<CheckCircle2 />}
            onClick={() =>
              toast({
                action: {
                  label: "Undo",
                  onClick: () => undefined,
                },
                description: "Workspace settings were saved.",
                title: "Saved",
                tone: "success",
              })
            }
          >
            Save
          </Button>
          <Button
            leftIcon={<AlertTriangle />}
            variant="outline"
            onClick={() =>
              toast({
                description: "The import worker stopped before finishing.",
                persistent: true,
                title: "Import failed",
                tone: "destructive",
              })
            }
          >
            Fail import
          </Button>
          <Button
            leftIcon={<ListChecks />}
            variant="outline"
            onClick={() =>
              toast({
                announcement: "CSV import is ready for review.",
                render: ({ dismiss }) => (
                  <div className="grid gap-3">
                    <div className="grid gap-1">
                      <div className="text-foreground font-medium">
                        CSV import ready
                      </div>
                      <div className="text-muted-foreground">
                        42 rows matched. 3 rows need review before publishing.
                      </div>
                    </div>
                    <Button size="sm" variant="outline" onClick={dismiss}>
                      Review later
                    </Button>
                  </div>
                ),
                tone: "info",
              })
            }
          >
            Custom toast
          </Button>
        </div>
      );
    }
    
    export function FeedbackToastWorkflow() {
      return (
        <div className="min-h-72">
          <ToastProvider motion="standard">
            <ToastControls />
            <ToastViewport />
          </ToastProvider>
        </div>
      );
    }

    Feedback state primitives share token-backed styling, stable slots, and reduced-motion-safe behavior.

    Feedback state props
    PropWhat it doesDefault
    LiveRegionProvider{ renderRegions, politeClassName, assertiveClassName }Provides one polite and one assertive live-region channel for announcements.{ renderRegions: true }
    useAnnouncer() => { announce, announcePolite, announceAssertive, clear }Imperative announcement API with debounce and coalescing options.Not set
    Spinner{ size, tone, variant, label }Decorative loading indicator by default; labelled instances expose role=status.{ size: "md", tone: "current", variant: "ring" }
    Progress{ value, min, max, indeterminate, label, status, showValue, tone }Linear progressbar with determinate, indeterminate, value label, and status text support.{ min: 0, max: 100 }
    ProgressCircleProgressProps & { thickness }Circular progress visual with the same value semantics as Progress.{ thickness: 8 }
    Skeleton{ animation: "pulse" | "shimmer" | "none", radius }Decorative layout-preserving placeholder with reduced-motion-safe animation.{ animation: "pulse", radius: "md" }
    Alert{ tone, variant, urgency, title, description, icon, actions, onDismiss }Inline status surface with optional urgent announcement semantics and action slots.{ tone: "info", variant: "soft", urgency: "polite" }
    CalloutAlertPropsContextual guidance surface that stays non-urgent unless explicitly configured.{ tone: "neutral", variant: "outline", urgency: "none" }
    EmptyState{ variant, tone, visual, title, description, primaryAction, secondaryAction }Composable empty, no-results, no-access, onboarding, and recovery state layout.{ variant: "card", tone: "neutral" }
    ToastProvider{ toasts, defaultToasts, placement, motion, maxToasts, defaultDuration }Owns toast state, announcements, timers, and the imperative useToast API.{ placement: "bottom-end", motion: "standard" }
    ToastRecord.renderReactNode | ({ toast, dismiss }) => ReactNodeOverrides default title, description, and action layout; pair with announcement for live-region copy.Not set
    ToastViewport{ placement, motion }Fixed, safe-area-aware stack surface for actionable and dismissible toasts.Not set