Exclusivebeta
Forgot — <mv-forgot>
Pre-send “you may have forgotten something” checker for composers and forms that, unlike a vague warning, says exactly what looks missing. Wrap a form (or point for= at one): declarative rules look at it as it is about to leave, and when one fires the send is held, a calm glowing orb lights up next to the send button and a small non-modal panel lists each likely omission with the words that triggered it highlighted in a one-line excerpt and its own fix: “Attach a file” opens your picker, “Add the link” puts the caret where the URL goes, “Fill it in” selects the leftover placeholder, a custom fix can even do the work (create the video link). Fixed items tick off live and the panel turns to “All set · Send now”; “Send anyway” always proceeds with the original submitter, so it never blocks for good; “Ignore” drops a reminder for this message and the opt-in “Don’t remind me” mutes a rule for good (remembered in the browser, or synced through mv-mute). Built-in starter rules: an attachment mentioned but none attached (English plus common French, German, Spanish and Italian words), a link mentioned without a URL, placeholders ([First name], {{company}}, TODO, TBD, lorem ipsum), an empty subject, a day or time mentioned while the date field is empty, a due date already past and too many recipients. Quoted replies are ignored. Custom rules are plain objects (test(state), or pattern + condition, message with {match}, fix), live mode lets the orb glow softly while typing without interrupting, and it runs after native validation: hard errors stay the browser's job, this only catches soft, likely omissions.
| Category | Forms |
|---|---|
| Type | Web Component (<mv-forgot>) |
| Status | beta |
| Also installs | button |
| Keywords | exclusive, culture, form, pre-send, send-check, attachment-reminder, forgotten-attachment, placeholder, missing-link, omission, reminder, composer, email, linter, soft-validation, send-anyway, submit, checklist |
When to use
- An email, message or support reply composer should catch “see attached” with no attachment or a leftover [First name] before it goes out
- A form sends something hard to take back (an invoice, a meeting invite, a release note) and common omissions should be caught before submit
- A PR, ticket or changelog template mentions screenshots, links or dates that authors often forget to actually add
- Teams want a gentle, dismissible pre-send check instead of making optional fields required
Avoid when
- The value is simply invalid (required, format, range) and must be fixed before anything is sent: use native validation and field errors → use Field instead
- The user is stuck and needs step-by-step guidance toward a goal, not a check at the moment of sending → use Insist instead
- The problem is invisible or look-alike characters pasted into a field → use Invisibles instead
Install
node scripts/add.mjs forgot --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["forgot"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, core/position.js, components/forgot/forgot.js, components/forgot/forgot.css, components/button/button.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="fg-demo" style="width:min(100%,60rem);margin-inline:auto">
<style>
#fg-demo { display:grid; gap:1.25rem; align-content:start }
#fg-demo .fg-card { min-width:0; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
#fg-demo .fg-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem; min-height:3rem; padding:.5rem 1rem .5rem 1.125rem; border-bottom:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#fg-demo .fg-bar h3 { margin:0; font-size:.875rem; font-weight:600; letter-spacing:-.01em }
#fg-demo .fg-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.75rem }
#fg-demo .fg-row { display:flex; align-items:center; gap:.75rem; padding:0 1.125rem; border-bottom:1px solid var(--mv-border) }
#fg-demo .fg-row label { flex:none; width:3.75rem; color:var(--mv-fg-muted); font-size:.8125rem }
#fg-demo .fg-row input { flex:1; min-width:0; height:2.625rem; padding:0; border:0; background:transparent; color:var(--mv-fg); font:inherit; font-size:.875rem; outline:none }
#fg-demo .fg-row:focus-within { box-shadow:inset 2px 0 0 var(--mv-accent) }
#fg-demo .fg-body { display:block; width:100%; box-sizing:border-box; min-height:13.5rem; padding:.875rem 1.125rem; border:0; background:transparent; color:var(--mv-fg); font:.875rem/1.6 var(--mv-font-sans); resize:vertical; outline:none }
#fg-demo .fg-body:focus-visible { box-shadow:inset 2px 0 0 var(--mv-accent) }
#fg-demo .fg-files { display:flex; flex-wrap:wrap; gap:.5rem; padding:0 1.125rem .75rem }
#fg-demo .fg-files:empty { display:none }
#fg-demo .fg-file { display:inline-flex; align-items:center; gap:.5rem; height:2rem; padding:0 .375rem 0 .625rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle); font-size:.75rem }
#fg-demo .fg-file svg { width:.875rem; height:.875rem; color:var(--mv-fg-muted) }
#fg-demo .fg-file small { color:var(--mv-fg-muted) }
#fg-demo .fg-file button { display:grid; place-items:center; width:1.375rem; height:1.375rem; padding:0; border:0; border-radius:var(--mv-radius-sm); background:none; color:var(--mv-fg-muted); cursor:pointer }
#fg-demo .fg-file button:hover { background:var(--mv-bg-muted); color:var(--mv-fg) }
#fg-demo .fg-foot { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; padding:.75rem 1rem .75rem 1.125rem; border-top:1px solid var(--mv-border) }
#fg-demo .fg-foot .fg-grow { flex:1 }
#fg-demo .fg-foot svg { width:1rem; height:1rem }
#fg-demo .fg-controls { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:.75rem 1.125rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#fg-demo .fg-controls .fg-actions { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
#fg-demo .fg-log { flex:1 1 16rem; min-height:1.125rem; margin:0; color:var(--mv-fg-muted); font:.75rem/1.4 var(--mv-font-mono) }
#fg-demo .mv-choice { font-size:.8125rem }
#fg-demo .fg-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.25rem; align-items:start }
#fg-demo .fg-form { display:grid; gap:.875rem; padding:1rem 1.125rem }
#fg-demo .fg-form .fg-two { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:.75rem }
#fg-demo .fg-form textarea { min-height:5.5rem; resize:vertical }
#fg-demo .fg-form .fg-foot { margin:.125rem -1.125rem -1rem; justify-content:flex-end }
#fg-demo .fg-amount { font-variant-numeric:tabular-nums }
@media (max-width:48rem) {
#fg-demo .fg-pair { grid-template-columns:minmax(0,1fr) }
}
@media (max-width:30rem) {
#fg-demo .fg-form .fg-two { grid-template-columns:minmax(0,1fr) }
}
</style>
<!-- Main: an email composer, checked live while typing and on Send -->
<section class="fg-card" aria-labelledby="fg-mail-title">
<div class="fg-bar">
<div>
<h3 id="fg-mail-title">New message</h3>
<p>Ledgerly · Customer Success</p>
</div>
<span class="mv-badge" data-variant="secondary">Draft saved 10:41 AM</span>
</div>
<mv-forgot id="fg-composer" live remember="demo-composer" max-recipients="12">
<form id="fg-mail" aria-labelledby="fg-mail-title">
<div class="fg-row">
<label for="fg-to">To</label>
<input id="fg-to" name="to" data-forgot="recipients" value="[email protected], [email protected]" autocomplete="off" spellcheck="false">
</div>
<div class="fg-row">
<label for="fg-subject">Subject</label>
<input id="fg-subject" name="subject" value="Signed SOW and next steps for the Q4 rollout" autocomplete="off">
</div>
<label class="mv-sr-only" for="fg-body">Message</label>
<textarea id="fg-body" class="fg-body" name="body" rows="9">Hi [First name],
Thanks again for the call on Tuesday. I’ve attached the signed statement of work for the Q4 rollout, along with the updated timeline.
You can review the onboarding checklist using the link below, and I’ll send the kickoff agenda by Friday. TODO: confirm the invoicing contact.
Best,
Lena Fischer
Customer Success · Ledgerly</textarea>
<div class="fg-files" id="fg-files" data-forgot="attachments" data-count="0" aria-label="Attachments"></div>
<div class="fg-foot">
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fg-attach" data-forgot-attach>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20.5 11.5-8.3 8.3a5.3 5.3 0 0 1-7.5-7.5l8.3-8.3a3.5 3.5 0 0 1 5 5l-8.3 8.3a1.8 1.8 0 0 1-2.5-2.5l7.6-7.6"/></svg>
Attach
</button>
<span class="fg-grow"></span>
<button type="submit" class="mv-button" data-variant="ghost" data-size="sm" data-forgot-skip>Save draft</button>
<button type="submit" class="mv-button" data-size="sm" id="fg-send">
Send
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-4 16-4-7z"/><path d="m12 13 8-9"/></svg>
</button>
</div>
</form>
</mv-forgot>
<div class="fg-controls">
<p class="fg-log" id="fg-log" aria-live="polite">Checked live: the glow next to Send says something may be missing.</p>
<span class="fg-actions">
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="fg-live" checked>
<span class="mv-choice-text"><span class="mv-choice-title">Live check</span></span>
</label>
<button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="fg-unmute">Forget muted rules</button>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fg-reset">Reset example</button>
</span>
</div>
</section>
<div class="fg-pair">
<!-- Variant: checked only when submitting, with a custom rule and its own verb -->
<section class="fg-card" aria-labelledby="fg-meet-title">
<div class="fg-bar">
<div>
<h3 id="fg-meet-title">New meeting</h3>
<p>Checked when you press Schedule</p>
</div>
</div>
<mv-forgot id="fg-meeting" verb="schedule" builtins="date, placeholder">
<form class="fg-form" id="fg-meet" aria-labelledby="fg-meet-title">
<div class="mv-field">
<label class="mv-label" for="fg-m-title">Title</label>
<input class="mv-input" id="fg-m-title" name="title" value="Q4 rollout walkthrough" required data-forgot-scan>
</div>
<div class="fg-two">
<div class="mv-field">
<label class="mv-label" for="fg-m-when">Date and time</label>
<input class="mv-input" id="fg-m-when" name="when" type="datetime-local" data-forgot="date">
</div>
<div class="mv-field">
<label class="mv-label" for="fg-m-where">Video link</label>
<input class="mv-input" id="fg-m-where" name="location" type="url" placeholder="https://" autocomplete="off">
</div>
</div>
<div class="mv-field">
<label class="mv-label" for="fg-m-desc">Description</label>
<textarea class="mv-textarea" id="fg-m-desc" name="description" rows="3">Let’s meet tomorrow at 3 pm to walk through the Q4 rollout plan with the Northwind team. We’ll join online and share the timeline on screen.</textarea>
</div>
<div class="fg-foot">
<button type="submit" class="mv-button" data-size="sm">Schedule</button>
</div>
</form>
</mv-forgot>
</section>
<!-- Variant: live, only the placeholder and past-date rules -->
<section class="fg-card" aria-labelledby="fg-inv-title">
<div class="fg-bar">
<div>
<h3 id="fg-inv-title">Invoice INV-2048</h3>
<p>Northwind Traders · <span class="fg-amount">$17,400.00</span></p>
</div>
</div>
<mv-forgot id="fg-invoice" live builtins="placeholder, past-date">
<form class="fg-form" id="fg-inv" aria-labelledby="fg-inv-title">
<div class="fg-two">
<div class="mv-field">
<label class="mv-label" for="fg-i-to">Bill to</label>
<input class="mv-input" id="fg-i-to" name="client" value="[email protected]" autocomplete="off">
</div>
<div class="mv-field">
<label class="mv-label" for="fg-i-due">Due date</label>
<input class="mv-input" id="fg-i-due" name="due" type="date" data-forgot="due">
</div>
</div>
<div class="mv-field">
<label class="mv-label" for="fg-i-note">Message</label>
<textarea class="mv-textarea" id="fg-i-note" name="note" rows="3">Hi {{client_first_name}}, here is invoice INV-2048 for the Q4 rollout ($17,400.00). Thank you for your business!</textarea>
</div>
<div class="fg-foot">
<button type="submit" class="mv-button" data-size="sm">Send invoice</button>
</div>
</form>
</mv-forgot>
</section>
</div>
<script type="module">
const composer = document.getElementById("fg-composer");
const mail = document.getElementById("fg-mail");
const files = document.getElementById("fg-files");
const log = document.getElementById("fg-log");
const meeting = document.getElementById("fg-meeting");
const invoice = document.getElementById("fg-invoice");
const time = () => new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
const say = (text) => { log.textContent = `${time()} · ${text}`; };
// A simulated uploader: one click attaches the signed PDF.
const clip = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3.5H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5z"/><path d="M14 3.5v5h5"/></svg>';
const sync = () => {
files.dataset.count = String(files.children.length);
files.dispatchEvent(new Event("change", { bubbles: true }));
};
document.getElementById("fg-attach").addEventListener("click", () => {
if (files.children.length) return;
const chip = document.createElement("span");
chip.className = "fg-file";
chip.innerHTML = `${clip}<span>SOW-Northwind-signed.pdf</span><small>284 KB</small>`;
const remove = document.createElement("button");
remove.type = "button";
remove.setAttribute("aria-label", "Remove SOW-Northwind-signed.pdf");
remove.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>';
remove.addEventListener("click", () => { chip.remove(); sync(); });
chip.append(remove);
files.append(chip);
sync();
});
// What the app sees.
composer.addEventListener("mv-forgot", (e) => {
const ids = e.detail.findings.map((f) => f.id).join(", ");
say(e.detail.trigger === "submit" ? `Send held: ${ids}.` : `Checked: ${ids}.`);
});
composer.addEventListener("mv-send-anyway", (e) => say(`Sent anyway despite ${e.detail.findings.length} reminder${e.detail.findings.length === 1 ? "" : "s"}.`));
composer.addEventListener("mv-mute", (e) => say(`Won’t remind about “${e.detail.id}” again (saved in this browser).`));
mail.addEventListener("submit", (e) => {
e.preventDefault();
if (e.submitter?.hasAttribute("data-forgot-skip")) { say("Draft saved, no check needed."); return; }
say("Message sent to 2 recipients.");
});
document.getElementById("fg-live").addEventListener("change", (e) => { composer.live = e.target.checked; });
document.getElementById("fg-unmute").addEventListener("click", () => { composer.unmute(); say("Muted rules forgotten: every reminder is back."); });
document.getElementById("fg-reset").addEventListener("click", () => {
mail.reset();
files.replaceChildren();
sync();
say("Example restored.");
});
// A custom rule: the invite says people join online, but there is no video link.
meeting.rules = [{
id: "video-link",
icon: "video",
pattern: /\b(?:join(?:ing)? online|video call|meet online|remotely)\b/i,
condition: (state) => !String(state.get("location")).trim(),
message: "The description says “{match}”, but there’s no video link.",
fix: {
label: "Create a video link",
run: (ctx) => {
const field = ctx.state.field("location");
field.value = "https://meet.northwind.io/q4-rollout-8fk2";
field.dispatchEvent(new Event("input", { bubbles: true }));
},
},
}];
document.getElementById("fg-meet").addEventListener("submit", (e) => { e.preventDefault(); say("Meeting scheduled."); });
document.getElementById("fg-inv").addEventListener("submit", (e) => { e.preventDefault(); say("Invoice INV-2048 sent."); });
// The invoice due date is nine days ago, whatever today is.
const due = document.getElementById("fg-i-due");
const past = new Date(Date.now() - 9 * 864e5);
due.value = due.defaultValue = `${past.getFullYear()}-${String(past.getMonth() + 1).padStart(2, "0")}-${String(past.getDate()).padStart(2, "0")}`;
due.dispatchEvent(new Event("change", { bubbles: true }));
// Show the mechanic right away: what the composer would say on Send.
customElements.whenDefined("mv-forgot").then(() => requestAnimationFrame(() => composer.check()));
</script>
</div>Cultural reference
Harry Potter and the Philosopher’s Stone — J. K. Rowling (1997, book). A small glass ball clouds over when its owner has forgotten something, but it cannot tell them what. In the UI, a calm glowing orb lights up next to the send button when something seems to be missing, and unlike the ball it says exactly what (the attachment, the link, the placeholder, the date) and offers the fix.
API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
live | boolean | false | Also evaluate while typing (debounced by delay): the orb glows softly next to the send button with the number of likely omissions and the send button's description mentions it, without opening anything or announcing. Without it, rules only run when sending (or through check()). |
builtins | "all" | "none" | list | all | Built-in rules to enable, comma or space separated: attachment, link, placeholder, subject, date, past-date, recipients. Each one only applies when the fields it needs exist (a date field for date, data-forgot="recipients" for recipients…). |
remember | string | Opt-in storage key: shows “Don’t remind me” on each finding and keeps muted rule ids in localStorage under mv-forgot:<key>. Without it, reminders can only be ignored for the current message. | |
send | CSS selector | Which submit button(s) are checked, inside the form. Default: every submitter except those with data-forgot-skip (e.g. Save draft); the first submitter anchors the orb and the panel. Without a form, the buttons whose clicks are held (default [data-forgot-send]). | |
for | id | Checks the form with this id instead of the form inside (or around) the element. | |
verb | string | send | Action word used in texts: “Before you schedule”, “Schedule anyway”, “Schedule now”. |
placement | top-end | top-start | top | bottom-end | bottom-start | bottom | top-end | Preferred side of the panel relative to the send button (flips and shifts to stay in the viewport). |
indicator | auto | none | auto | auto inserts the orb right before the send button, or inside your own [data-forgot-indicator] element if there is one (framework-friendly); none shows no orb, only the panel. |
max-recipients | number | 10 | Above this many addresses in the data-forgot="recipients" fields, the recipients rule fires (“This will go to 42 people.”). |
delay | number (ms) | 450 | Debounce of the live evaluation after the last keystroke. While a send is held, fixes are re-checked after 160 ms. |
data-forgot | body | subject | attachments | link | date | due | recipients | On a field, gives it a role for the built-in rules. Defaults without it: the first textarea or contenteditable is the body, name="subject" the subject, input[type=url] the link field, date / datetime-local inputs the date, every input[type=file] counts as attachments. On a custom uploader, data-forgot="attachments" with data-count="n" (dispatch a change event when it changes). | |
data-forgot-attach / data-forgot-future / data-forgot-scan / data-forgot-skip / data-forgot-send | marker attributes | data-forgot-attach: the button the “Attach a file” fix clicks (otherwise the file input). data-forgot-future: a date field that must not be in the past (like data-forgot="due"). data-forgot-scan: another text field whose text the rules read. data-forgot-skip: a submitter that is never checked. data-forgot-send: the send buttons of a region without a form. | |
data-state / data-count / data-open / data-clear | set by the component | On the host: idle | live | flagged | clear, the number of likely omissions, while the panel is open, during the short “All set” after everything was fixed. |
Properties
| Name | Type | Description |
|---|---|---|
rules | Array<{ id, test?(state), pattern?, in?, condition?(state, match), message, field?, fix?, icon? }> | Custom rules, added to the built-ins (same id replaces one). test(state) returns falsy, true, a message string or { message?, match?, field?, vars?, fix? }; or give pattern (RegExp or word) + condition(state, match) returning true when something is missing. message may use {match} and your vars, or be a function (state, match). fix: { label, run(ctx) } or { label, field } (focus) or { label, click } (selector). icon: clip, link, brackets, subject, calendar, clock, people, video, image, dot. state: { form, root, fields, subject, body, text, attachments, hasLink, recipients, today, get(name), field(name), find(pattern, where?), findAll(pattern, where?) }. |
strings | Partial<Record<string, string>> | Overrides every visible text, announcement and built-in rule message ({verb} / {Verb} / {count} / {s} / {match} placeholders). Keys: title, textOne, textOther, allSetTitle, allSetText, sendAnyway, send, keepEditing, ignore, mute, showMe, resolved, indicatorFlagged, indicatorClear, indicatorName, indicatorClearName, sendHint, announce, announceCheck, announceAllSet, announceIgnored, announceMuted, attachment, attachmentFix, link, linkFix, placeholder, placeholderMany, placeholderFix, subject, subjectFix, date, dateFix, pastDate, pastDateFix, recipients, recipientsFix. |
muted | string[] | Rule ids the user asked not to be reminded about. Read it, or set it from your server's preferences (persisted with remember). |
findings | Array<{ id, message, match, field }> | Current likely omissions, ignored and muted rules excluded (read-only). |
target | HTMLFormElement | HTMLElement | The form (or region) being checked (read-only). |
isOpen | boolean | True while the panel is open (read-only). |
Methods
| Name | Description |
|---|---|
check({ open? }) | Checks now, like a send would (emits the cancelable mv-forgot, flags the orb, opens the panel unless open: false) and returns the findings. For composers without a form: if (forgot.check().length) return; then send on mv-send-anyway. |
evaluate() | Runs every rule without any UI or event and returns the findings. |
sendAnyway() | Proceeds despite the findings: emits mv-send-anyway, then re-submits with the original submitter (requestSubmit, so native validation still applies) or clicks the send button. |
open() / close() | Opens the panel on the held findings, or folds it back (focus returns to the send button when it was inside). |
unmute(id?) | Reminds again about one rule, or every muted rule without an argument. |
Events
| Name | Description |
|---|---|
mv-forgot | Cancelable, before a send is held. detail: { findings: [{ id, message, match, field }], trigger: "submit" | "click" | "check" | "indicator" }. preventDefault() lets the send go through untouched (e.g. an internal recipient). |
mv-send-anyway | “Send anyway” was chosen while reminders were still open, just before the form is re-submitted. detail: { findings } (the ones overridden, for analytics). |
mv-forgot-change | The live findings changed (live mode, or while a send is held). detail: { findings }. |
mv-mute | “Don’t remind me” was chosen. detail: { id, muted } (all muted ids), to sync the preference to your server. |
Content structure
| Name | Description |
|---|---|
(content) | The form to check (or any region with fields and data-forgot-send buttons). The orb is inserted before the send button (or into [data-forgot-indicator]), the panel right after it so Tab reaches it next; both leave with the element. |
CSS classes
| Name | Description |
|---|---|
mv-forgot-indicator | The orb button (<button>, aria-expanded): .mv-forgot-orb (with .mv-forgot-ripple), .mv-forgot-tick, .mv-forgot-count, .mv-forgot-label; data-state="live | flagged | clear". |
mv-forgot-panel | Non-modal dialog (Popover API top layer): .mv-forgot-head (-head-orb, -title, -text), .mv-forgot-list of .mv-forgot-item (data-rule, data-resolved) with -item-icon, -msg, -quote (.mv-forgot-mark), -item-actions (.mv-forgot-fix, .mv-forgot-link), and .mv-forgot-actions (.mv-forgot-keep, .mv-forgot-go). data-all-set once everything is fixed. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-forgot-glow | var(--mv-warning) | Color of the orb, its glow, the finding icons and the highlighted words. |
--mv-forgot-clear | var(--mv-success) | Color of the “All set” state. |
Accessibility
Nothing is ever blocked for good and nothing relies on color. When a send is held, focus stays on the send button and an assertive live region says what may be missing (“Before you send, check 3 things. You wrote “attached”, but nothing is attached. … Choose Send anyway to send as is.”); on-demand checks are announced politely and live mode never announces, it only adds a visually hidden description to the send button (“3 things may be missing before you send.”) so screen reader users hear it when they reach Send. The panel is a labelled, described non-modal dialog inserted right after the send button in the DOM, so Tab goes from Send straight to the first finding; every finding offers real buttons (its fix, “Ignore”, and “Don’t remind me” when remember is set) described by the finding's message, and the excerpt shows the triggering words in a <mark>. Fixes that select text or focus a field move focus there and fold the panel; a fix that opens a picker keeps it open. Resolved findings get a check icon and the word “Fixed”, and focus moves on to the next one; Escape or an outside click folds the panel and focus returns to the send button when it was inside; “Keep editing” goes to the first omission. The orb is a <button> with aria-expanded whose accessible name includes its visible count (“3 possible omissions before you send. Review”); it states its meaning with a number and a label, not a color. Runs after native constraint validation, so real errors keep the browser's own messages. Reduced motion (OS or data-motion="reduce"): the glow is static, no swell or ripple, the panel appears without scaling. Forced colors: the orb and highlights use system colors (Highlight, Mark).