Dirty

Unsaved changes exist.

Type to change the value — a dot marks unsaved changes. The toggle forces it for inspection.

html

<label class="dty" data-dirty="false" style="--accent:#E8A020;">
  <span class="dty__label">Project name</span>
  <input class="dty__input" type="text" value="Untitled" oninput="this.closest('.dty').dataset.dirty = String(this.value !== this.defaultValue)">
</label>
<p class="dty__hint">Type to change the value — a dot marks unsaved changes. The toggle forces it for inspection.</p>
<style>
.dty { display: block; width: 280px; box-sizing: border-box; font: 14px/1.5 system-ui, sans-serif; color: #241430; background: #FAF7F2; padding: 16px 18px; border-radius: 12px; }
.dty__label { display: flex; align-items: center; gap: 6px; margin-bottom: 7px; font-size: 12px; font-weight: 600; color: #241430aa; }
.dty__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); display: inline-block; }
.dty__input { width: 100%; box-sizing: border-box; font: 14px system-ui, sans-serif; padding: 9px 11px; border-radius: 8px; border: 1.5px solid #24143030; }
.dty[data-dirty="true"] .dty__input { border-color: var(--accent); }
.dty__hint { width: 280px; box-sizing: border-box; margin: 8px 0 0; font-size: 11px; color: #241430888; }
</style>

tailwind

<label class="dty-tw block w-[280px] box-border font-sans text-sm text-[#241430] bg-[#FAF7F2] px-[18px] py-4 rounded-xl" data-dirty="false">
  <span class="flex items-center gap-1.5 mb-1.5 text-xs font-semibold text-[#241430]/65">
    Project name
    
  </span>
  <input type="text" value="Untitled" class="w-full box-border text-sm px-2.5 py-2 rounded-lg border-[1.5px] border-[#24143030]" oninput="this.closest('.dty-tw').dataset.dirty = String(this.value !== this.defaultValue)">
</label>
<p class="w-[280px] box-border mt-2 text-[11px] text-[#241430]/55">Type to change the value — a dot marks unsaved changes. The toggle forces it for inspection.</p>
<style>
.dty-tw[data-dirty="true"] input { border-color: #E8A020; }
</style>

react

import { useState } from "react";

export function DirtyState({
  label = "Project name",
  value = "Untitled",
  accent = "#E8A020",
  force = false,
}) {
  const [current, setCurrent] = useState(value);
  const dirty = force || current !== value;
  return (
    <div>
      <label style={{ display: "block", width: 280, boxSizing: "border-box", font: "14px/1.5 system-ui, sans-serif", color: "#241430", background: "#FAF7F2", padding: "16px 18px", borderRadius: 12 }}>
        <span style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 7, fontSize: 12, fontWeight: 600, color: "#241430aa" }}>
          {label}
          {dirty && <span aria-hidden="true" style={{ width: 7, height: 7, borderRadius: "50%", background: accent, display: "inline-block" }} />}
        </span>
        <input type="text" value={current} onChange={(e) => setCurrent(e.target.value)}
          style={{ width: "100%", boxSizing: "border-box", font: "14px system-ui, sans-serif", padding: "9px 11px", borderRadius: 8, border: `1.5px solid ${dirty ? accent : "#24143030"}` }} />
      </label>
      <p style={{ width: 280, margin: "8px 0 0", fontSize: 11, color: "#241430888" }}>Type to change the value — a dot marks unsaved changes. The <code>force</code> prop forces it for inspection.</p>
    </div>
  );
}