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)
default-example.tsx
1"use client";23import { 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";1011export function StateFlowDefaultExample() {12 const [currentState, setCurrentState] = useState<StateFlowState>("idle");1314 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.div18 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>3031 <StateFlowDiagram currentState={currentState} />3233 <div className="mt-12 flex flex-wrap gap-3 justify-center">34 <MotionButton35 variant="info"36 onClick={() => setCurrentState("loading")}37 disabled={currentState === "loading"}38 >39 Start Loading40 </MotionButton>4142 <MotionButton43 variant="success"44 onClick={() => setCurrentState("success")}45 disabled={currentState !== "loading"}46 >47 Success48 </MotionButton>4950 <MotionButton51 variant="danger"52 onClick={() => setCurrentState("error")}53 disabled={currentState !== "loading"}54 >55 Error56 </MotionButton>5758 <MotionButton59 variant="neutral"60 onClick={() => setCurrentState("idle")}61 disabled={currentState === "idle"}62 >63 Reset64 </MotionButton>65 </div>6667 <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
async-journey-example.tsx
1"use client";23import { 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";1112type SimulationOutcome = "success" | "error" | "random";13type SimulationScenario = "form-submit" | "api-request" | "background-job";1415interface ScenarioConfig {16 label: string;17 description: string;18 processingLabel: string;19 successLabel: string;20 errorLabel: string;21 idleHint: string;22 loadingHint: string;23}2425const 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};5455export 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);6768 const scenarioConfig = useMemo(() => SCENARIO_CONFIG[scenario], [scenario]);6970 useEffect(() => {71 return () => {72 if (timerRef.current !== null) {73 window.clearTimeout(timerRef.current);74 }75 };76 }, []);7778 const pushLog = (message: string) => {79 setRequestLog((prev) => [message, ...prev].slice(0, 6));80 };8182 const clearPendingReset = () => {83 if (timerRef.current !== null) {84 window.clearTimeout(timerRef.current);85 timerRef.current = null;86 }87 };8889 const resetToIdle = (message = "Reset to idle.") => {90 activeRunRef.current += 1;91 clearPendingReset();92 setCurrentState("idle");93 setLastDurationMs(null);94 pushLog(message);95 };9697 const simulateRequest = async () => {98 const runId = activeRunRef.current + 1;99 activeRunRef.current = runId;100 clearPendingReset();101102 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";106107 setCurrentState("loading");108 setRunCount((prev) => prev + 1);109 pushLog(110 `${scenarioConfig.processingLabel} (${latencyMs}ms simulated latency).`,111 );112113 await new Promise((resolve) => {114 window.setTimeout(resolve, latencyMs);115 });116117 if (activeRunRef.current !== runId) {118 return;119 }120121 const duration = Math.round(performance.now() - startedAt);122 setLastDurationMs(duration);123124 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 }131132 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 };142143 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 Playground150 </h3>151 <p className="mt-1 text-sm text-slate-400">152 Replace generic spinners with a visible lifecycle debugger for153 async UI work.154 </p>155 </div>156 <AnimatePresence mode="wait">157 <motion.div158 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>171172 <div className="mt-5 grid gap-3 md:grid-cols-3">173 {(Object.keys(SCENARIO_CONFIG) as SimulationScenario[]).map((key) => (174 <button175 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 === key182 ? "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>195196 <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 Outcome199 </label>200 <select201 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 <input214 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 completion220 </label>221 </div>222223 <div className="mt-8 flex justify-center overflow-x-auto pb-2">224 <StateFlowDiagram225 currentState={currentState}226 labels={{227 idle: scenarioConfig.idleHint,228 loading: scenarioConfig.loadingHint,229 success: scenarioConfig.successLabel,230 error: scenarioConfig.errorLabel,231 }}232 />233 </div>234235 <div className="mt-8 flex flex-wrap gap-3 justify-center">236 <MotionButton237 variant="info"238 onClick={simulateRequest}239 disabled={currentState === "loading"}240 >241 Run Simulated Async Flow242 </MotionButton>243244 <MotionButton245 variant="neutral"246 onClick={() =>247 resetToIdle("Manual reset triggered to inspect initial state.")248 }249 disabled={currentState === "idle" && lastDurationMs === null}250 >251 Reset252 </MotionButton>253 </div>254255 <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 Timeline259 </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
| Name | Type | Default | Description |
|---|---|---|---|
| state | string | idle | The current state of the flow |
| label | string | Idle | The label of the current state |
| description | string | Ready | The description of the current state |
| active | boolean | false | Whether the current state is active |
| direction | string | horizontal | The direction of the flow |
| isActive | boolean | false | Whether the current state is active |
| intent | string | primary | The intent of the flow |