State Machine

Formal model of states and transitions.

Document lifecycle

Current Draft
submit
Current In review
approve
Current Approved
publish
Current Published

Currently: in review

html

<div class="state-machine" data-current="in review" style="--accent:#241430;--gap:28px;">
  <h3 class="state-machine__title">Document lifecycle</h3>
  <div class="state-machine__diagram">
    <div class="state-machine__node state-machine__node--draft">
      <span class="state-machine__current" aria-current="step">Current</span>
      <span class="state-machine__name">Draft</span>
    </div>
    <div class="state-machine__edge">
      <svg class="state-machine__arrow" viewBox="0 0 40 10" aria-hidden="true"><path d="M0 5h32M26 1l6 4-6 4" fill="none" stroke="#241430" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      <span class="state-machine__label">submit</span>
    </div>
    <div class="state-machine__node state-machine__node--in-review">
      <span class="state-machine__current" aria-current="step">Current</span>
      <span class="state-machine__name">In review</span>
    </div>
    <div class="state-machine__edge">
      <svg class="state-machine__arrow" viewBox="0 0 40 10" aria-hidden="true"><path d="M0 5h32M26 1l6 4-6 4" fill="none" stroke="#241430" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      <span class="state-machine__label">approve</span>
    </div>
    <div class="state-machine__node state-machine__node--approved">
      <span class="state-machine__current" aria-current="step">Current</span>
      <span class="state-machine__name">Approved</span>
    </div>
    <div class="state-machine__edge">
      <svg class="state-machine__arrow" viewBox="0 0 40 10" aria-hidden="true"><path d="M0 5h32M26 1l6 4-6 4" fill="none" stroke="#241430" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      <span class="state-machine__label">publish</span>
    </div>
    <div class="state-machine__node state-machine__node--published">
      <span class="state-machine__current" aria-current="step">Current</span>
      <span class="state-machine__name">Published</span>
    </div>
  </div>
  <p class="state-machine__caption">Currently: <strong>in review</strong></p>
</div>
<style>
.state-machine { width: 640px; max-width: 100%; padding: 20px; background: #FAF7F2; color: #241430; font: 14px/1.4 system-ui, sans-serif; border-radius: 14px; box-shadow: 0 8px 24px rgba(36,20,48,.12); box-sizing: border-box; }
.state-machine__title { margin: 0 0 16px; font-size: 16px; }
.state-machine__diagram { display: flex; align-items: center; gap: var(--gap); flex-wrap: wrap; }
.state-machine__node { position: relative; padding: 10px 14px; border-radius: 10px; background: #24143008; border: 2px solid transparent; text-align: center; min-width: 88px; }
.state-machine__name { display: block; font-weight: 700; font-size: 13px; }
.state-machine__current { display: none; position: absolute; top: -10px; left: 50%; transform: translateX(-50%); background: var(--accent); color: #FAF7F2; font-size: 10px; font-weight: 700; padding: 1px 8px; border-radius: 999px; text-transform: uppercase; letter-spacing: .04em; }
.state-machine__edge { display: flex; flex-direction: column; align-items: center; gap: 2px; flex-shrink: 0; }
.state-machine__arrow { width: 34px; height: 9px; }
.state-machine__label { font-size: 11px; color: #241430b3; font-style: italic; }
.state-machine[data-current="draft"] .state-machine__node--draft,
.state-machine[data-current="in review"] .state-machine__node--in-review,
.state-machine[data-current="approved"] .state-machine__node--approved,
.state-machine[data-current="published"] .state-machine__node--published {
  border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, #FAF7F2);
}
.state-machine[data-current="draft"] .state-machine__node--draft .state-machine__current,
.state-machine[data-current="in review"] .state-machine__node--in-review .state-machine__current,
.state-machine[data-current="approved"] .state-machine__node--approved .state-machine__current,
.state-machine[data-current="published"] .state-machine__node--published .state-machine__current {
  display: inline-block;
}
</style>

tailwind

<div class="state-machine-tw w-[640px] max-w-full p-5 bg-[#FAF7F2] text-[#241430] font-sans text-sm rounded-2xl shadow-lg box-border" data-current="in review" style="--accent:#241430;--gap:28px;">
  <h3 class="m-0 mb-4 text-base">Document lifecycle</h3>
  <div class="flex items-center flex-wrap" style="gap:var(--gap);">
    <div class="state-machine-tw__node state-machine-tw__node--draft relative text-center min-w-[88px] px-3.5 py-2.5 rounded-[10px] bg-[#24143008] border-2 border-transparent">
      <span class="state-machine-tw__current absolute -top-2.5 left-1/2 -translate-x-1/2 text-[10px] font-bold px-2 py-px rounded-full uppercase tracking-wide" style="background:#241430;color:#FAF7F2;" aria-current="step">Current</span>
      <span class="block font-bold text-[13px]">Draft</span>
    </div>
    <div class="flex flex-col items-center gap-0.5 flex-shrink-0">
      <svg class="w-[34px] h-[9px]" viewBox="0 0 40 10" aria-hidden="true"><path d="M0 5h32M26 1l6 4-6 4" fill="none" stroke="#241430" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      <span class="text-[11px] italic text-[#241430]/70">submit</span>
    </div>
    <div class="state-machine-tw__node state-machine-tw__node--in-review relative text-center min-w-[88px] px-3.5 py-2.5 rounded-[10px] bg-[#24143008] border-2 border-transparent">
      <span class="state-machine-tw__current absolute -top-2.5 left-1/2 -translate-x-1/2 text-[10px] font-bold px-2 py-px rounded-full uppercase tracking-wide" style="background:#241430;color:#FAF7F2;" aria-current="step">Current</span>
      <span class="block font-bold text-[13px]">In review</span>
    </div>
    <div class="flex flex-col items-center gap-0.5 flex-shrink-0">
      <svg class="w-[34px] h-[9px]" viewBox="0 0 40 10" aria-hidden="true"><path d="M0 5h32M26 1l6 4-6 4" fill="none" stroke="#241430" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      <span class="text-[11px] italic text-[#241430]/70">approve</span>
    </div>
    <div class="state-machine-tw__node state-machine-tw__node--approved relative text-center min-w-[88px] px-3.5 py-2.5 rounded-[10px] bg-[#24143008] border-2 border-transparent">
      <span class="state-machine-tw__current absolute -top-2.5 left-1/2 -translate-x-1/2 text-[10px] font-bold px-2 py-px rounded-full uppercase tracking-wide" style="background:#241430;color:#FAF7F2;" aria-current="step">Current</span>
      <span class="block font-bold text-[13px]">Approved</span>
    </div>
    <div class="flex flex-col items-center gap-0.5 flex-shrink-0">
      <svg class="w-[34px] h-[9px]" viewBox="0 0 40 10" aria-hidden="true"><path d="M0 5h32M26 1l6 4-6 4" fill="none" stroke="#241430" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      <span class="text-[11px] italic text-[#241430]/70">publish</span>
    </div>
    <div class="state-machine-tw__node state-machine-tw__node--published relative text-center min-w-[88px] px-3.5 py-2.5 rounded-[10px] bg-[#24143008] border-2 border-transparent">
      <span class="state-machine-tw__current absolute -top-2.5 left-1/2 -translate-x-1/2 text-[10px] font-bold px-2 py-px rounded-full uppercase tracking-wide" style="background:#241430;color:#FAF7F2;" aria-current="step">Current</span>
      <span class="block font-bold text-[13px]">Published</span>
    </div>
  </div>
  <p class="mt-4 mb-0 text-[13px]">Currently: <strong>in review</strong></p>
</div>
<style>
.state-machine-tw__current { display: none; }
.state-machine-tw[data-current="draft"] .state-machine-tw__node--draft,
.state-machine-tw[data-current="in review"] .state-machine-tw__node--in-review,
.state-machine-tw[data-current="approved"] .state-machine-tw__node--approved,
.state-machine-tw[data-current="published"] .state-machine-tw__node--published {
  border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, #FAF7F2);
}
.state-machine-tw[data-current="draft"] .state-machine-tw__node--draft .state-machine-tw__current,
.state-machine-tw[data-current="in review"] .state-machine-tw__node--in-review .state-machine-tw__current,
.state-machine-tw[data-current="approved"] .state-machine-tw__node--approved .state-machine-tw__current,
.state-machine-tw[data-current="published"] .state-machine-tw__node--published .state-machine-tw__current {
  display: inline-block;
}
</style>

react

export function StateMachine({
  title = "Document lifecycle",
  current = "in review",
  accent = "#241430",
  compact = false,
  nodeGap = 28,
}) {
  const nodes = [
    { key: "draft", name: "Draft" },
    { key: "in review", name: "In review" },
    { key: "approved", name: "Approved" },
    { key: "published", name: "Published" },
  ];
  const edges = ["submit", "approve", "publish"];
  return (
    <div style={{ width: 640, maxWidth: "100%", padding: 20, background: "#FAF7F2", color: "#241430",
      fontFamily: "system-ui, sans-serif", fontSize: 14, borderRadius: 14, boxShadow: "0 8px 24px rgba(36,20,48,.12)",
      boxSizing: "border-box" }}>
      <h3 style={{ margin: "0 0 16px", fontSize: 16 }}>{title}</h3>
      <div style={{ display: "flex", alignItems: "center", gap: nodeGap, flexWrap: "wrap" }}>
        {nodes.map((node, i) => (
          <span key={node.key} style={{ display: "inline-flex", alignItems: "center", gap: nodeGap }}>
            <span style={{ position: "relative", textAlign: "center", minWidth: 88, padding: "10px 14px",
              borderRadius: 10, display: "inline-block",
              background: current === node.key ? `color-mix(in srgb, ${accent} 14%, #FAF7F2)` : "#24143008",
              border: current === node.key ? `2px solid ${accent}` : "2px solid transparent" }}>
              {current === node.key && (
                <span aria-current="step" style={{ position: "absolute", top: -10, left: "50%", transform: "translateX(-50%)",
                  background: accent, color: "#FAF7F2", fontSize: 10, fontWeight: 700, padding: "1px 8px",
                  borderRadius: 999, textTransform: "uppercase", letterSpacing: "0.04em" }}>Current</span>
              )}
              <span style={{ display: "block", fontWeight: 700, fontSize: 13 }}>{node.name}</span>
            </span>
            {i < edges.length && (
              <span style={{ display: "inline-flex", flexDirection: "column", alignItems: "center", gap: 2 }}>
                <svg width="34" height="9" viewBox="0 0 40 10" aria-hidden="true"><path d="M0 5h32M26 1l6 4-6 4" fill="none" stroke="#241430" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
                {!compact && <span style={{ fontSize: 11, fontStyle: "italic", color: "#241430b3" }}>{edges[i]}</span>}
              </span>
            )}
          </span>
        ))}
      </div>
      <p style={{ margin: "16px 0 0", fontSize: 13 }}>Currently: <strong>{current}</strong></p>
    </div>
  );
}