<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/css" href="https://www.aaron-gustafson.com/c/feed.min.css" ?><feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:amg="https://www.aaron-gustafson.com.com/amg-dtd/"><title>Aaron Gustafson: Content tagged UX</title><subtitle>The latest 20 posts and links tagged UX.</subtitle><id>https://www.aaron-gustafson.com</id><link href="https://www.aaron-gustafson.com/feeds/ux.xml" rel="self"/><link href="https://www.aaron-gustafson.com"/><author><name>Aaron Gustafson</name><uri>https://www.aaron-gustafson.com</uri></author><updated>2026-04-22T20:17:47Z</updated><entry><id>https://www.aaron-gustafson.com/notebook/visual-validation-feedback-for-form-fields/</id><title type="html"><![CDATA[✍🏻 Visual Validation Feedback for Form Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/visual-validation-feedback-for-form-fields/" rel="alternate" type="text/html" /><published>2026-04-22T20:17:47Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Password requirements, username rules, input format constraints: forms often have multiple validation requirements, but users frequently do not find out whether they are meeting them until they hit submit. The <code>form-validation-list</code> web component changes that by providing real-time visual feedback as users type, showing exactly which requirements are met and which are not.</p><p><ins datetime="2026-04-30T00:00:00+00:00"><strong>Update:</strong> This post has been refreshed to cover the component’s current loading options, throttled input behavior, accessibility model, and localization hooks.</ins></p><p>This is a modern replacement for my old <a href="https://github.com/easy-designs/jquery.easy-validation-rules.js">jQuery Easy Validation Rules</a> plugin, reimagined as a web component with native form validation integration.</p><hr><p>To get started, associate the component with an <code>input</code> element using the <code>for</code> attribute and define your validation rules:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Username:<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[A-Z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one capital letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[a-z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one lowercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[\d]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Submit<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>By default, validation runs on the <code>input</code> event with a 250ms throttle. Matched rules get a checkmark (✓), unmatched rules get an X (✗), and while someone is typing the component announces a concise progress summary instead of repeatedly re-reading the whole rule list. When all rules match, the field is valid and the form can be submitted.</p><h2 id="what%E2%80%99s-happening-under-the-hood%3F" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-happening-under-the-hood%3F" aria-hidden="true">#</a> What’s happening under the hood?</h2><p>The component:</p><ol><li>Associates with an input via the <code>for</code> attribute (just like a <code>label</code> element)</li><li>Finds all elements with <code>data-pattern</code> attributes</li><li>Tests the input value against each pattern when the configured trigger fires</li><li>Adds <code>validation-matched</code> or <code>validation-unmatched</code> classes and visual indicators accordingly</li><li>Inserts localized, visually hidden state text once the field has a value</li><li>Updates a single polite live region while users type</li><li>Uses <code>setCustomValidity()</code> to integrate with native form validation</li><li>Prevents form submission until all rules match</li></ol><p>The cascade animation, controlled by <code>each-delay</code>, creates a pleasant visual effect as rules are checked sequentially. It is a small touch, but a nice one.</p><h2 id="whose-rules%3F-your-rules." tabindex="-1"><a class="header-anchor" href="#whose-rules%3F-your-rules." aria-hidden="true">#</a> Whose rules? Your rules.</h2><p>Define rules using regular expression patterns in the <code>data-pattern</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Length requirements --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>.{8,}<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least 8 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>.{8,32}<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Between 8 and 32 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Character type requirements --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[A-Z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one uppercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[a-z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one lowercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[\d]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[!@#$%^&amp;<em>]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one special character<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Format requirements --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.+@.+..+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Valid email format<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span><sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup>+$<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Only letters and numbers<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>Each pattern is a standard JavaScript regular expression. The component tests the <code>input</code> value against all patterns on the configured trigger, using throttled <code>input</code> events by default.</p><h2 id="input-event-too-noisy%3F-no-worries." tabindex="-1"><a class="header-anchor" href="#input-event-too-noisy%3F-no-worries." aria-hidden="true">#</a> Input event too noisy? No worries.</h2><p>By default, validation runs on the <code>input</code> event with a 250ms throttle. If you want immediate feedback while typing, set <code>input-throttle=&quot;0&quot;</code>. If you’d rather wait until the field loses focus, switch the <code>trigger-event</code> to <code>&quot;blur&quot;</code>:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token attr-name">trigger-event</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>blur<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.+@.+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Contains @ symbol<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.+@.+..+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Valid email format<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>With this attribute in place, validation runs immediately when the field loses focus. In this mode, <code>input-throttle</code> is ignored and the component keeps the full criteria list available to assistive technology while someone types.</p><h2 id="wanna-adjust-the-cascade-delay%3F-go-for-it." tabindex="-1"><a class="header-anchor" href="#wanna-adjust-the-cascade-delay%3F-go-for-it." aria-hidden="true">#</a> Wanna adjust the cascade delay? Go for it.</h2><p>Use the <code>each-delay</code> attribute to control the delay between checking each rule. The default speed is 150ms, but you can tune it to any number of milliseconds:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">each-delay</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>100<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- rules --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>Set it to “0” to remove the cascade effect entirely and check all rules simultaneously.</p><h2 id="need-full-design-control%3F-you-got-it." tabindex="-1"><a class="header-anchor" href="#need-full-design-control%3F-you-got-it." aria-hidden="true">#</a> Need full design control? You got it.</h2><p>If you want full design control over the component, you can absolutely have it. The whole component operates in light DOM, so your styles will pierce through. And you can customize <code>class</code> names for integration with CSS frameworks using a set of attributes on the <code>form-validation-list</code> element. The <code>field-valid-class</code> and <code>field-invalid-class</code> attributes control the class names applied to the <code>input</code> field itself, while the <code>rule-matched-class</code> and <code>rule-unmatched-class</code> attributes control the <code>class</code> names applied to each rule item.</p><p>Here’s a complete example:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">&gt;</span></span><span class="token style"><span class="token language-css"><span class="token selector">.is-valid</span><span class="token punctuation">{</span><span class="token property">border-color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token selector">.is-invalid</span><span class="token punctuation">{</span><span class="token property">border-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token selector">.rule-pass</span><span class="token punctuation">{</span><span class="token property">color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token selector">.rule-fail</span><span class="token punctuation">{</span><span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><span class="token punctuation">}</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>username<span class="token punctuation">“</span></span><span class="token attr-name">field-valid-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>is-valid<span class="token punctuation">“</span></span><span class="token attr-name">field-invalid-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>is-invalid<span class="token punctuation">“</span></span><span class="token attr-name">rule-matched-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>rule-pass<span class="token punctuation">“</span></span><span class="token attr-name">rule-unmatched-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>rule-fail<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.{5,}<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least 5 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[!@#]+<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span>Special char (!@#)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>This approach lets you use <code>class</code> names that match your existing CSS architecture, rather than making one small component dictate terms to the rest of your styles.</p><p>You can also override the per-instance icon glyphs with the <code>rule-matched-icon</code> and <code>rule-unmatched-icon</code> attributes, or control the shared visual styling using CSS custom properties:</p><ul><li><code>–rule-matched-icon</code> - Content for matched state (default: “✓”)</li><li><code>–rule-unmatched-icon</code> - Content for unmatched state (default: “✗”)</li><li><code>–rule-icon-size</code> - Size of icons (default: 1em)</li><li><code>–rule-matched-color</code> - Color for matched rules (default: green)</li><li><code>–rule-unmatched-color</code> - Color for unmatched rules (default: red)</li></ul><p>The older <code>–validation-</em></code> custom property names are still supported as legacy aliases.</p><p>Here’s an example of that:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token selector">form-validation-list</span><span class="token punctuation">{</span><span class="token property">–rule-matched-icon</span><span class="token punctuation">:</span><span class="token string">”✅“</span><span class="token punctuation">;</span><span class="token property">–rule-unmatched-icon</span><span class="token punctuation">:</span><span class="token string">”❌“</span><span class="token punctuation">;</span><span class="token property">–rule-icon-size</span><span class="token punctuation">:</span> 1.2em<span class="token punctuation">;</span><span class="token property">–rule-matched-color</span><span class="token punctuation">:</span> #28a745<span class="token punctuation">;</span><span class="token property">–rule-unmatched-color</span><span class="token punctuation">:</span> #dc3545<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><h2 id="typescript-or-framework-project%3F-you%E2%80%99re-covered." tabindex="-1"><a class="header-anchor" href="#typescript-or-framework-project%3F-you%E2%80%99re-covered." aria-hidden="true">#</a> TypeScript or framework project? You’re covered.</h2><p>The package now ships with bundled type definitions and reflects its core properties and attributes in both directions. That makes it a much better fit for TypeScript, JSX, SSR, and declarative framework setups where properties may be assigned before the custom element upgrades.</p><h2 id="bit-of-a-control-freak%3F-there%E2%80%99s-an-api." tabindex="-1"><a class="header-anchor" href="#bit-of-a-control-freak%3F-there%E2%80%99s-an-api." aria-hidden="true">#</a> Bit of a control freak? There’s an API.</h2><p>If you really want to get into the weeds, you can also listen for validation changes in your JavaScript code:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> validationList <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-validation-list”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>validationList<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-validation-list:validated”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">const</span><span class="token punctuation">{</span> isValid<span class="token punctuation">,</span> matchedRules<span class="token punctuation">,</span> totalRules<span class="token punctuation">,</span> field <span class="token punctuation">}</span><span class="token operator">=</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Matched &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;matchedRules&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; of &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;totalRules&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; rules&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Field is &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;isValid &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;valid&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;invalid&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>The event fires after validation completes and gives you the current state. Nice and tidy.</p><p>You can also manually trigger validation and check the element’s current state at any time:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> validationList <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-validation-list”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Trigger validation</span><span class="token keyword">const</span> isValid <span class="token operator">=</span> validationList<span class="token punctuation">.</span><span class="token function">validate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Is valid:”</span><span class="token punctuation">,</span> isValid<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Check current state</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Current state:”</span><span class="token punctuation">,</span> validationList<span class="token punctuation">.</span>isValid<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="global-site%3F-relaje!" tabindex="-1"><a class="header-anchor" href="#global-site%3F-relaje!" aria-hidden="true">#</a> Global site? <i lang="es">Relaje!</i></h2><p>If you need the component to work in different languages, that’s totally doable. You can customize three separate pieces of copy: the browser validation message (<code>validation-message</code>), the live summary announced while typing (<code>announcement</code>), and the per-rule hidden status text (<code>rule-matched-alt</code> and <code>rule-unmatched-alt</code>). All of the message templates support the <code>{matched}</code> and <code>{total}</code> placeholders:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token comment">&lt;!-- Spanish --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>contrasena<span class="token punctuation">“</span></span><span class="token attr-name">announcement</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>{matched} de {total} criterios cumplidos<span class="token punctuation">“</span></span><span class="token attr-name">rule-matched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Criterio cumplido<span class="token punctuation">“</span></span><span class="token attr-name">rule-unmatched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Criterio pendiente<span class="token punctuation">“</span></span><span class="token attr-name">validation-message</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Por favor, cumple todos los requisitos ({matched} de {total})<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[A-Z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Al menos una letra mayúscula<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[a-z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Al menos una letra minúscula<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[\d]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Al menos un número<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- French --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>mot-de-passe<span class="token punctuation">“</span></span><span class="token attr-name">announcement</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>{matched} critères satisfaits sur {total}<span class="token punctuation">“</span></span><span class="token attr-name">rule-matched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Critère satisfait<span class="token punctuation">“</span></span><span class="token attr-name">rule-unmatched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Critère non satisfait<span class="token punctuation">“</span></span><span class="token attr-name">validation-message</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Veuillez satisfaire à toutes les exigences ({matched} sur {total})<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[A-Z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Au moins une lettre majuscule<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[a-z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Au moins une lettre minuscule<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[\d]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Au moins un chiffre<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><h2 id="is-it-a-progressive-enhancement%3F-heck-yeah!" tabindex="-1"><a class="header-anchor" href="#is-it-a-progressive-enhancement%3F-heck-yeah!" aria-hidden="true">#</a> Is it a progressive enhancement? Heck yeah!</h2><p>The component uses light DOM, so if JavaScript fails, users still see the validation requirements as a standard list. They can read what is expected even without the visual feedback. Your server-side validation still does the important enforcement work regardless… right? <em>Right?</em></p><h2 id="is-it-screen-reader-accessible%3F-yep." tabindex="-1"><a class="header-anchor" href="#is-it-screen-reader-accessible%3F-yep." aria-hidden="true">#</a> Is it screen reader accessible? Yep.</h2><p>The component is built with accessibility in mind:</p><ul><li><strong>Proper description support</strong>: The validation list is automatically associated with the <code>input</code> via <code>aria-describedby</code>, and if the field already has <code>aria-describedby</code>, the original value is preserved.</li><li><strong>A concise announcement model</strong>: With the default <code>trigger-event=&quot;input&quot;</code>, the component temporarily suspends the full criteria list from <code>aria-describedby</code> while someone types and uses a single polite live region to announce progress instead.</li><li><strong>State restoration on blur</strong>: When focus leaves the field, any pending validation timeouts are cleared and the full criteria list is restored so returning to the field announces the final criteria state.</li><li><strong>Localized rule state</strong>: Once the field has a value, each rule gets visually hidden localized state text in the DOM, which is more robust than relying on CSS-generated content alone.</li></ul><p>If you have suggestions for other ways to improve the accessibility of this component, please <a href="https://github.com/aarongustafson/form-validation-list/issues">open an issue on GitHub</a>.</p><h2 id="does-it-integrate-with-the-browser%E2%80%99s-validation-engine%3F-naturally." tabindex="-1"><a class="header-anchor" href="#does-it-integrate-with-the-browser%E2%80%99s-validation-engine%3F-naturally." aria-hidden="true">#</a> Does it integrate with the browser’s validation engine? Naturally.</h2><p>The component uses <code>setCustomValidity()</code> to participate in native form validation:</p><ul><li>When all rules match, custom validity is cleared</li><li>When rules don’t match, a custom validity message is set</li><li>Form submission is prevented until all rules pass</li><li>Works with <code>:valid</code> and <code>:invalid</code> CSS pseudo-classes</li><li>Compatible with the Constraint Validation API</li></ul><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> form <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> field <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">“username”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>form<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“submit”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>form<span class="token punctuation">.</span><span class="token function">checkValidity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Validation failed:”</span><span class="token punctuation">,</span> field<span class="token punctuation">.</span>validationMessage<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="here%E2%80%99s-a-real-world-example" tabindex="-1"><a class="header-anchor" href="#here%E2%80%99s-a-real-world-example" aria-hidden="true">#</a> Here’s a real-world example</h2><p>Here’s a complete password validation setup:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Password:<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.{8,}<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least 8 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[A-Z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one uppercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[a-z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one lowercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[\d]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[!@#$%^&amp;<em>]+<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span>At least one special character (!@#$%^&amp;</em>)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Submit<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>Users see exactly which requirements they have met and which they still need to satisfy. That tends to be a lot kinder than springing the whole list on them after submit.</p><h2 id="play-with-it" tabindex="-1"><a class="header-anchor" href="#play-with-it" aria-hidden="true">#</a> Play with it</h2><p>Check out <a href="https://aarongustafson.github.io/form-validation-list/demo/">the demo</a> with various examples:</p><figure id="fig-2025-12-06-09" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-validation-list/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>View the project on <a href="https://github.com/aarongustafson/form-validation-list">GitHub</a>.</p><p>Install via <code>npm</code>:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-validation-list</code></pre><p>For most projects, import the guarded auto-definition helper:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/form-validation-list/define.js”</span><span class="token punctuation">;</span></code></pre><p>If you want to control the tag name yourself, import <code>FormValidationListElement</code> and register it manually.</p><p>Happy validating!</p><hr class="footnotes-sep"><section class="footnotes"><h4 class="hidden">Footnotes</h4><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>a-zA-Z0-9 <a href="#fnref1" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content><amg:twitter><![CDATA[New #WebComponent: Show users which validation requirements they’ve met — as they type.]]></amg:twitter><amg:summary><![CDATA[The <code>form-validation-list</code> web component provides real-time visual feedback on validation requirements, showing users which rules they have satisfied as they type.]]></amg:summary><summary type="html"><![CDATA[<p>The <code>form-validation-list</code> web component provides real-time visual feedback on validation requirements, showing users which rules they have satisfied as they type.</p>]]></summary><category term="web components" /><category term="progressive enhancement" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="web forms" /><category term="UX" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/never-lose-form-progress-again/</id><title type="html"><![CDATA[✍🏻 Never Lose Form Progress Again]]></title><link href="https://www.aaron-gustafson.com/notebook/never-lose-form-progress-again/" rel="alternate" type="text/html" /><published>2026-04-20T23:59:08Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Few things are more annoying than losing your progress halfway through a form. Maybe the browser crashes. Maybe the tab gets closed. Maybe your kid yells from the other room and you come back three hours later wondering why you ever thought now was a good time to fill out a mortgage application. Whatever the cause, <code>form-saver</code> makes those interruptions a lot less obnoxious. Which is nice, because forms are usually annoying enough on their own.</p><p>At its core, <code>form-saver</code> is a small web component that wraps a form, keeps an eye on it, stores values in <code>localStorage</code>, and restores them when the page loads again. Better yet, it clears out saved data after a successful submission so you’re not accidentally resurrecting stale information the next time someone stops by. Nobody wants yesterday’s half-finished support request shambling back to life.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>All you need to do is wrap your form in the component:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Message<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>textarea</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>message<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>textarea</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Send<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>That’s it. The component targets the first descendant <code>form</code>, saves values as users type or make changes, and restores them when they come back. No extra plumbing. Just a form with a slightly better memory than most of us have before coffee — depending on the day, that may not be a terribly high bar, but still.</p><p>This is especially handy for forms that are a little more involved than a simple email signup. Job applications, checkout flows, support requests, and multi-question onboarding forms all benefit from a little resilience. So do the people filling them out, who generally have better things to do than retype the same answers because a tab got squirrelly.</p><h2 id="what-actually-gets-saved%3F" tabindex="-1"><a class="header-anchor" href="#what-actually-gets-saved%3F" aria-hidden="true">#</a> What actually gets saved?</h2><p><code>form-saver</code> supports the controls most of us reach for every day:</p><ul><li>Text-style <code>input</code> fields</li><li><code>textarea</code> elements,</li><li><code>select</code> elements (including multi-selects), and</li><li><code>checkbox</code> and <code>radio</code> controls.</li></ul><p>File inputs are intentionally excluded.</p><p>Because the component works in light DOM, your form remains your form. Your labels, validation, layout, and CSS continue to work exactly as they did before. <code>form-saver</code> just adds a bit of memory and, ideally, cuts down on a few muttered curses.</p><h2 id="want-to-keep-a-few-fields-after-submit%3F" tabindex="-1"><a class="header-anchor" href="#want-to-keep-a-few-fields-after-submit%3F" aria-hidden="true">#</a> Want to keep a few fields after submit?</h2><p>In many cases, clearing everything after a successful submission is the right call. Sometimes, though, it makes sense to keep a few details around. Maybe you want to preserve a visitor’s name and email address on a contact form while clearing the message body. That way they do not have to keep retyping the boring bits. Nobody wakes up excited to enter their email address for the fourth time.</p><p>That is what the <code>retain</code> attribute is for:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">retain</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name email<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>After a successful submission, <code>name</code> and <code>email</code> stick around, but <code>message</code> gets cleared. Simple, sensible, and less likely to leave someone staring at your form like it just betrayed them personally.</p><h2 id="better-yet%2C-let-users-decide" tabindex="-1"><a class="header-anchor" href="#better-yet%2C-let-users-decide" aria-hidden="true">#</a> Better yet, let users decide</h2><p>Persisting form data can be incredibly helpful, but there is a human side to this too. Just because we <em>can</em> keep someone’s information around does not necessarily mean we <em>should</em> do it without asking. That is where <code>retain-choice</code> comes in. It lets you be useful without getting presumptuous.</p><p>Add it alongside <code>retain</code> and <code>form-saver</code> will inject an opt-in checkbox for the user. Nice and easy:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">retain</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name email<span class="token punctuation">”</span></span><span class="token attr-name">retain-choice</span><span class="token attr-name">retain-choice-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Store my contact information for later<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>By default, that checkbox is inserted just before the first submit control. If the user leaves it unchecked, the retained fields are cleared along with everything else after submit. If they opt in, those selected fields remain. Their call, as it should be. Gotta love a little informed consent.</p><p>Need to place that control somewhere more appropriate in your layout? Use <code>retain-choice-container</code> to point to a CSS selector:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">retain</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name email<span class="token punctuation">”</span></span><span class="token attr-name">retain-choice</span><span class="token attr-name">retain-choice-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Remember my details next time<span class="token punctuation">”</span></span><span class="token attr-name">retain-choice-container</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>.form-footer<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>form-footer<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Send<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>That gives you a lot more control over the experience without making you build the retention UI yourself.</p><h2 id="need-a-custom-storage-key%3F" tabindex="-1"><a class="header-anchor" href="#need-a-custom-storage-key%3F" aria-hidden="true">#</a> Need a custom storage key?</h2><p>By default, <code>form-saver</code> derives its storage key from the wrapped form’s method and action, which is usually exactly what you want. It keeps different forms from stepping on one another and keeps the setup nice and boring. Boring is good.</p><p>If you need something more explicit, you can provide your own <code>storage-key</code>:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">storage-key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>checkout:shipping-address<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/checkout/shipping<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Street Address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>street-address<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>street-address<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Postal Code<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>postal-code<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>postal-code<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Continue<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>This is useful when a form’s URL is not stable or when you want multiple views to intentionally share the same saved state. Sometimes explicit is just easier. Sometimes it is the only way to stay sane.</p><h2 id="want-to-drive-it-yourself%3F" tabindex="-1"><a class="header-anchor" href="#want-to-drive-it-yourself%3F" aria-hidden="true">#</a> Want to drive it yourself?</h2><p>If you need more direct control, the component exposes a few methods:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> saver <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-saver”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Persist the current state</span>saver<span class="token punctuation">.</span><span class="token function">saveFormState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Restore previously saved values</span>saver<span class="token punctuation">.</span><span class="token function">restoreFormState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Clear out anything stored for this form</span>saver<span class="token punctuation">.</span><span class="token function">clearSavedData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>That can be useful when you want to pair it with your own UI, analytics, or some custom workflow around save and restore. Or when you just like being the one driving and do not fully trust anything labeled “automatic.”</p><h2 id="progressive-enhancement%2C-as-usual" tabindex="-1"><a class="header-anchor" href="#progressive-enhancement%2C-as-usual" aria-hidden="true">#</a> Progressive enhancement, as usual</h2><p>This component follows a pattern I am always going to favor: start with a perfectly ordinary form, then layer on the enhancement. If JavaScript fails, the form still works. Users can still fill it out and submit it. They just will not get the recovery behavior. Annoying, perhaps, but not catastrophic. And that is very much the point.</p><p>That’s a pretty good trade-off.</p><p>And because saved values are only cleared after a successful submit flow, you do not lose everything just because client-side validation blocked submission or some other script got clever at exactly the wrong moment. That matters. A lot of “smart” form experiences are only smart right up until they are not.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>If you want to kick the tires, I put together <a href="https://aarongustafson.github.io/form-saver/demo/">a live demo</a> with examples of the retention options as well:</p><figure id="fig-2026-04-20-01" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-saver/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>The project is available on <a href="https://github.com/aarongustafson/form-saver">GitHub</a>, and you can install it from npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-saver</code></pre><p>If you want the easiest path, just import it and let the component register itself:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/form-saver”</span><span class="token punctuation">;</span></code></pre><p>If you would rather define it yourself, you can import the class directly:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> FormSaverElement <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/form-saver/form-saver.js”</span><span class="token punctuation">;</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">“form-saver”</span><span class="token punctuation">,</span> FormSaverElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Either way, you wind up with a more forgiving form experience and a little less needless frustration for the people filling it out. Which, in my book, is a pretty solid deal. The bar for delight in forms is often just “don’t make me do that again,” and honestly, I’ll take it.</p>]]></content><amg:twitter><![CDATA[Browsers crash. Tabs close. Life happens. Here’s a web component that saves form progress so your users don’t have to start over from scratch.]]></amg:twitter><amg:summary><![CDATA[Browsers crash. Tabs close. Life happens. Here’s a web component that saves form progress so your users don’t have to start over from scratch.]]></amg:summary><summary type="html"><![CDATA[<p>Browsers crash. Tabs close. Life happens. Here’s a web component that saves form progress so your users don’t have to start over from scratch.</p>]]></summary><category term="web components" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web forms" /><category term="UX" /></entry></feed>