Search

Search the site

All components

State Flow

Simulated API and processing states for visual debugging of async UI transitions.

Default
Default state flow
Current State: idle
IdleReady
>
LoadingProcessing...
>
SuccessComplete!
v
ErrorFailed

idle → loading → success

loading → error (on failure)

1"use client";
2
3import { useState } from "react";
4import { motion, AnimatePresence } from "framer-motion";
5import {
6 StateFlowDiagram,
7 type StateFlowState,
8} from "@/registry/ui/state-flow";
9import { MotionButton } from "./motion-button";
10
11export function StateFlowDefaultExample() {
12 const [currentState, setCurrentState] = useState<StateFlowState>("idle");
13
14 return (
15 <div className="min-h-screen bg-slate-950 flex flex-col items-center justify-center p-8">
16 <AnimatePresence mode="wait">
17 <motion.div
18 key={currentState}
19 initial={{ opacity: 0, y: -20 }}
20 animate={{ opacity: 1, y: 0 }}
21 exit={{ opacity: 0, y: 20 }}
22 className="mb-8 px-4 py-2 rounded-full bg-slate-800 border border-slate-700"
23 >
24 <span className="text-sm text-slate-400">Current State: </span>
25 <span className="text-sm font-semibold text-white capitalize">
26 {currentState}
27 </span>
28 </motion.div>
29 </AnimatePresence>
30
31 <StateFlowDiagram currentState={currentState} />
32
33 <div className="mt-12 flex flex-wrap gap-3 justify-center">
34 <MotionButton
35 variant="info"
36 onClick={() => setCurrentState("loading")}
37 disabled={currentState === "loading"}
38 >
39 Start Loading
40 </MotionButton>
41
42 <MotionButton
43 variant="success"
44 onClick={() => setCurrentState("success")}
45 disabled={currentState !== "loading"}
46 >
47 Success
48 </MotionButton>
49
50 <MotionButton
51 variant="danger"
52 onClick={() => setCurrentState("error")}
53 disabled={currentState !== "loading"}
54 >
55 Error
56 </MotionButton>
57
58 <MotionButton
59 variant="neutral"
60 onClick={() => setCurrentState("idle")}
61 disabled={currentState === "idle"}
62 >
63 Reset
64 </MotionButton>
65 </div>
66
67 <div className="mt-8 text-xs text-slate-500 text-center">
68 <p>idle → loading → success</p>
69 <p className="mt-1">loading → error (on failure)</p>
70 </div>
71 </div>
72 );
73}
Async State Journey
Simulated API and processing states for visual debugging of async UI transitions.

Async State Journey Playground

Replace generic spinners with a visible lifecycle debugger for async UI work.

Current State: idle
IdleReady to submit
>
LoadingCreating account...
>
SuccessUser created
v
ErrorValidation failed

Timeline

Choose a scenario and run a simulation.

Scenario: Form Submission

Runs: 0

Last duration: N/A

1"use client";
2
3import { useEffect, useMemo, useRef, useState } from "react";
4import { motion, AnimatePresence } from "framer-motion";
5import { cn } from "@/lib/utils";
6import {
7 StateFlowDiagram,
8 type StateFlowState,
9} from "@/registry/ui/state-flow";
10import { MotionButton } from "./motion-button";
11
12type SimulationOutcome = "success" | "error" | "random";
13type SimulationScenario = "form-submit" | "api-request" | "background-job";
14
15interface ScenarioConfig {
16 label: string;
17 description: string;
18 processingLabel: string;
19 successLabel: string;
20 errorLabel: string;
21 idleHint: string;
22 loadingHint: string;
23}
24
25const SCENARIO_CONFIG: Record<SimulationScenario, ScenarioConfig> = {
26 "form-submit": {
27 label: "Form Submission",
28 description: "Simulate a sign up request with validation + network delay.",
29 processingLabel: "Submitting",
30 successLabel: "User created",
31 errorLabel: "Validation failed",
32 idleHint: "Ready to submit",
33 loadingHint: "Creating account...",
34 },
35 "api-request": {
36 label: "API Request",
37 description: "Simulate a data fetch and render-ready status update.",
38 processingLabel: "Fetching",
39 successLabel: "Data received",
40 errorLabel: "Request timeout",
41 idleHint: "Waiting for request",
42 loadingHint: "Request in flight...",
43 },
44 "background-job": {
45 label: "Background Job",
46 description: "Simulate async processing work queued on the server.",
47 processingLabel: "Processing",
48 successLabel: "Job complete",
49 errorLabel: "Job failed",
50 idleHint: "Queue idle",
51 loadingHint: "Worker is running...",
52 },
53};
54
55export function StateFlowAsyncJourneyExample() {
56 const [scenario, setScenario] = useState<SimulationScenario>("form-submit");
57 const [outcome, setOutcome] = useState<SimulationOutcome>("random");
58 const [currentState, setCurrentState] = useState<StateFlowState>("idle");
59 const [runCount, setRunCount] = useState(0);
60 const [lastDurationMs, setLastDurationMs] = useState<number | null>(null);
61 const [autoReset, setAutoReset] = useState(false);
62 const [requestLog, setRequestLog] = useState<string[]>([
63 "Choose a scenario and run a simulation.",
64 ]);
65 const timerRef = useRef<number | null>(null);
66 const activeRunRef = useRef(0);
67
68 const scenarioConfig = useMemo(() => SCENARIO_CONFIG[scenario], [scenario]);
69
70 useEffect(() => {
71 return () => {
72 if (timerRef.current !== null) {
73 window.clearTimeout(timerRef.current);
74 }
75 };
76 }, []);
77
78 const pushLog = (message: string) => {
79 setRequestLog((prev) => [message, ...prev].slice(0, 6));
80 };
81
82 const clearPendingReset = () => {
83 if (timerRef.current !== null) {
84 window.clearTimeout(timerRef.current);
85 timerRef.current = null;
86 }
87 };
88
89 const resetToIdle = (message = "Reset to idle.") => {
90 activeRunRef.current += 1;
91 clearPendingReset();
92 setCurrentState("idle");
93 setLastDurationMs(null);
94 pushLog(message);
95 };
96
97 const simulateRequest = async () => {
98 const runId = activeRunRef.current + 1;
99 activeRunRef.current = runId;
100 clearPendingReset();
101
102 const startedAt = performance.now();
103 const latencyMs = 900 + Math.floor(Math.random() * 1400);
104 const shouldSucceed =
105 outcome === "random" ? Math.random() > 0.35 : outcome === "success";
106
107 setCurrentState("loading");
108 setRunCount((prev) => prev + 1);
109 pushLog(
110 `${scenarioConfig.processingLabel} (${latencyMs}ms simulated latency).`,
111 );
112
113 await new Promise((resolve) => {
114 window.setTimeout(resolve, latencyMs);
115 });
116
117 if (activeRunRef.current !== runId) {
118 return;
119 }
120
121 const duration = Math.round(performance.now() - startedAt);
122 setLastDurationMs(duration);
123
124 if (shouldSucceed) {
125 setCurrentState("success");
126 pushLog(`${scenarioConfig.successLabel} in ${duration}ms.`);
127 } else {
128 setCurrentState("error");
129 pushLog(`${scenarioConfig.errorLabel} after ${duration}ms.`);
130 }
131
132 if (autoReset) {
133 timerRef.current = window.setTimeout(() => {
134 if (activeRunRef.current === runId) {
135 setCurrentState("idle");
136 setLastDurationMs(null);
137 pushLog("Auto-reset returned the flow to idle.");
138 }
139 }, 1600);
140 }
141 };
142
143 return (
144 <div className="min-h-screen bg-slate-950 flex flex-col items-center justify-center p-8">
145 <div className="w-full max-w-5xl rounded-2xl border border-slate-800 bg-slate-900/40 p-6">
146 <div className="flex flex-wrap items-center justify-between gap-4">
147 <div>
148 <h3 className="text-lg font-semibold text-white">
149 Async State Journey Playground
150 </h3>
151 <p className="mt-1 text-sm text-slate-400">
152 Replace generic spinners with a visible lifecycle debugger for
153 async UI work.
154 </p>
155 </div>
156 <AnimatePresence mode="wait">
157 <motion.div
158 key={currentState}
159 initial={{ opacity: 0, y: -6 }}
160 animate={{ opacity: 1, y: 0 }}
161 exit={{ opacity: 0, y: 6 }}
162 className="rounded-full border border-slate-700 bg-slate-800 px-4 py-2 text-sm"
163 >
164 <span className="text-slate-400">Current State: </span>
165 <span className="font-semibold text-white capitalize">
166 {currentState}
167 </span>
168 </motion.div>
169 </AnimatePresence>
170 </div>
171
172 <div className="mt-5 grid gap-3 md:grid-cols-3">
173 {(Object.keys(SCENARIO_CONFIG) as SimulationScenario[]).map((key) => (
174 <button
175 key={key}
176 type="button"
177 onClick={() => setScenario(key)}
178 disabled={currentState === "loading"}
179 className={cn(
180 "rounded-lg border px-4 py-3 text-left transition-colors",
181 scenario === key
182 ? "border-blue-500 bg-blue-500/10"
183 : "border-slate-700 bg-slate-900/60 hover:border-slate-500",
184 )}
185 >
186 <p className="text-sm font-medium text-white">
187 {SCENARIO_CONFIG[key].label}
188 </p>
189 <p className="mt-1 text-xs text-slate-400">
190 {SCENARIO_CONFIG[key].description}
191 </p>
192 </button>
193 ))}
194 </div>
195
196 <div className="mt-4 flex flex-wrap items-center gap-3 rounded-xl border border-slate-800 bg-slate-900/70 p-3">
197 <label className="text-xs font-medium uppercase tracking-wide text-slate-400">
198 Simulated Outcome
199 </label>
200 <select
201 value={outcome}
202 onChange={(event) =>
203 setOutcome(event.target.value as SimulationOutcome)
204 }
205 disabled={currentState === "loading"}
206 className="rounded-md border border-slate-700 bg-slate-950 px-3 py-1.5 text-sm text-white"
207 >
208 <option value="random">Random</option>
209 <option value="success">Force Success</option>
210 <option value="error">Force Error</option>
211 </select>
212 <label className="ml-auto inline-flex items-center gap-2 text-sm text-slate-300">
213 <input
214 type="checkbox"
215 checked={autoReset}
216 onChange={(event) => setAutoReset(event.target.checked)}
217 className="h-4 w-4 rounded border-slate-700 bg-slate-950"
218 />
219 Auto reset after completion
220 </label>
221 </div>
222
223 <div className="mt-8 flex justify-center overflow-x-auto pb-2">
224 <StateFlowDiagram
225 currentState={currentState}
226 labels={{
227 idle: scenarioConfig.idleHint,
228 loading: scenarioConfig.loadingHint,
229 success: scenarioConfig.successLabel,
230 error: scenarioConfig.errorLabel,
231 }}
232 />
233 </div>
234
235 <div className="mt-8 flex flex-wrap gap-3 justify-center">
236 <MotionButton
237 variant="info"
238 onClick={simulateRequest}
239 disabled={currentState === "loading"}
240 >
241 Run Simulated Async Flow
242 </MotionButton>
243
244 <MotionButton
245 variant="neutral"
246 onClick={() =>
247 resetToIdle("Manual reset triggered to inspect initial state.")
248 }
249 disabled={currentState === "idle" && lastDurationMs === null}
250 >
251 Reset
252 </MotionButton>
253 </div>
254
255 <div className="mt-8 grid gap-4 md:grid-cols-[1fr_auto]">
256 <div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
257 <p className="text-xs font-medium uppercase tracking-wide text-slate-500">
258 Timeline
259 </p>
260 <div className="mt-3 space-y-2 text-sm text-slate-300">
261 {requestLog.map((entry, index) => (
262 <p key={`${entry}-${index}`}>{entry}</p>
263 ))}
264 </div>
265 </div>
266 <div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 text-sm text-slate-300">
267 <p>
268 <span className="text-slate-500">Scenario:</span>{" "}
269 {scenarioConfig.label}
270 </p>
271 <p className="mt-2">
272 <span className="text-slate-500">Runs:</span> {runCount}
273 </p>
274 <p className="mt-2">
275 <span className="text-slate-500">Last duration:</span>{" "}
276 {lastDurationMs ? `${lastDurationMs}ms` : "N/A"}
277 </p>
278 </div>
279 </div>
280 </div>
281 </div>
282 );
283}

Installation & source

Install via the shadcn CLI or copy the registry files manually.

bash
npx shadcn@latest add @tt-ui/state-flow

Props

NameTypeDefaultDescription
statestringidleThe current state of the flow
labelstringIdleThe label of the current state
descriptionstringReadyThe description of the current state
activebooleanfalseWhether the current state is active
directionstringhorizontalThe direction of the flow
isActivebooleanfalseWhether the current state is active
intentstringprimaryThe intent of the flow