<?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 forms</title><subtitle>The latest 20 posts and links tagged forms.</subtitle><id>https://www.aaron-gustafson.com</id><link href="https://www.aaron-gustafson.com/feeds/forms.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-05-25T22:23:24Z</updated><entry><id>https://www.aaron-gustafson.com/notebook/matching-form-fields-with-a-web-component/</id><title type="html"><![CDATA[✍🏻 Easy Data-entry Verification with a Web Component]]></title><link href="https://www.aaron-gustafson.com/notebook/matching-form-fields-with-a-web-component/" rel="alternate" type="text/html" /><published>2026-05-25T22:23:24Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Sometimes the simplest form pattern is the one you repeat <i lang="la">ad infinitum</i>: “Type it once, then type it again.” We do it for password confirmations, email verification, and any flow where a typo creates expensive follow-up work. I released a small web component to handle that pattern cleanly: <a href="https://github.com/aarongustafson/form-matching-fields"><code>form-matching-fields</code></a>.</p><p>The component wraps two related form fields and adds validation to ensure they match. It’s intentionally additive, which means it works with native browser validation rather than trying to replace it.</p><h2 id="wrap-and-go" tabindex="-1"><a class="header-anchor" href="#wrap-and-go" aria-hidden="true">#</a> Wrap and go</h2><p>The API is intentionally simple:</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-matching-fields</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">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">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">required</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-again<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Password again<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password-again<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>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-matching-fields</span><span class="token punctuation">&gt;</span></span></code></pre><p>The component evaluates the first two eligible text-type inputs it contains and applies mismatch validation to the second one when both fields have values.</p><h2 id="what-counts-as-an-eligible-field%3F" tabindex="-1"><a class="header-anchor" href="#what-counts-as-an-eligible-field%3F" aria-hidden="true">#</a> What counts as an eligible field?</h2><p>To keep behavior predictable, the component only considers descendant <code>input</code> elements of these types:</p><ul><li>text</li><li>email</li><li>password</li><li>search</li><li>tel</li><li>url</li></ul><p>It ignores <code>disabled</code> and <code>readonly</code> controls, which helps prevent false positives when you have conditional or locked fields in the same wrapper.</p><h2 id="validation-behavior-that-plays-nicely" tabindex="-1"><a class="header-anchor" href="#validation-behavior-that-plays-nicely" aria-hidden="true">#</a> Validation behavior that plays nicely</h2><p>One of the core goals here was to avoid stepping on existing validation rules. In practice, that means:</p><ul><li>If the second field already has a native validation issue (like <code>required</code> or <code>type</code> mismatch), this component won’t mask it.</li><li>If the second field already has a custom validity message, this component won’t overwrite that either.</li><li>It only clears mismatch errors that it set itself.</li></ul><p>That last point is especially useful in larger forms where multiple constraints overlap. You don’t want one helper trying to be the sole source of truth.</p><h2 id="customizing-the-validation-message" tabindex="-1"><a class="header-anchor" href="#customizing-the-validation-message" aria-hidden="true">#</a> Customizing the validation message</h2><p>By default, the component uses this template for the validation message:</p><blockquote><p>The fields “{label_1}” and “{label_2}” should match</p></blockquote><p>It smartly resolves the labels (<code>{label_1}</code> and <code>{label_2}</code>) from your markup in this order:</p><ol><li>associated <code>&lt;label for&gt;</code> text</li><li>wrapping <code>&lt;label&gt;</code> text</li><li><code>aria-label</code></li><li><code>name</code></li><li><code>id</code></li></ol><p>You can override it with the <code>validation-message</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-matching-fields</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>Please ensure {label_2} matches {label_1}.<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 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 punctuation">&gt;</span></span>Email<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">id</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">required</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>verify-email<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Verify email<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>verify-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">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-matching-fields</span><span class="token punctuation">&gt;</span></span></code></pre><p>In most forms, that gives you a clear error message without extra setup.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I put together <a href="https://aarongustafson.github.io/form-matching-fields/demo/">a demo of the web component</a> over on GitHub:</p><figure class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-matching-fields/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>You can explore the source, file issues, and suggest enhancements in <a href="https://github.com/aarongustafson/form-matching-fields">the <code>form-matching-fields</code> repository</a>.</p>]]></content><amg:twitter><![CDATA[Need two form fields to match? `form-matching-fields` adds simple validation for confirmation fields.]]></amg:twitter><amg:summary><![CDATA[<p>Sometimes the simplest form pattern is the one you repeat <i lang="la">ad infinitum</i>: “Type it once, then type it again.” We do it for password confirmations, email verification, and any flow where a typo creates expensive follow-up work. I released a small web component to handle that pattern cleanly: <a href="https://github.com/aarongustafson/form-matching-fields"><code>form-matching-fields</code></a>.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Sometimes the simplest form pattern is the one you repeat <i lang="la">ad infinitum</i>: “Type it once, then type it again.” We do it for password confirmations, email verification, and any flow where a typo creates expensive follow-up work. I released a small web component to handle that pattern cleanly: <a href="https://github.com/aarongustafson/form-matching-fields"><code>form-matching-fields</code></a>.</p>]]></summary><category term="accessibility" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/matching-form-fields-with-a-web-component.jpg" /></entry><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, and format constraints tend to pile up fast. Too often, people only learn whether they met them after they hit submit. The <code>form-validation-list</code> web component changes that by providing real-time feedback as someone types, showing exactly which requirements are met, and which still need work.</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>, adds a subtle sequential effect as rules are checked. Small detail, meaningful feedback.</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 instead of 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-rel%C3%A1jese." tabindex="-1"><a class="header-anchor" href="#global-site%3F-rel%C3%A1jese." aria-hidden="true">#</a> Global site? <i lang="es">Relájese.</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 is usually 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>If you give it a spin, I’d love to hear how it works in your forms. 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[<p>Password requirements, username rules, and format constraints tend to pile up fast. Too often, people only learn whether they met them after they hit submit. The <code>form-validation-list</code> web component changes that by providing real-time feedback as someone types, showing exactly which requirements are met, and which still need work.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Password requirements, username rules, and format constraints tend to pile up fast. Too often, people only learn whether they met them after they hit submit. The <code>form-validation-list</code> web component changes that by providing real-time feedback as someone types, showing exactly which requirements are met, and which still need work.</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="user experience" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/visual-validation-feedback-for-form-fields.jpg" /></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 closes. Maybe your kid yells from the other room, and you come back three hours later wondering why you 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 painful, which helps because forms are usually frustrating 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 people 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 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 read their credit card number aloud in a bus station.</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. Just because we <em>can</em> keep someone’s information around doesn’t mean we <em>should</em> do it without asking. That is where <code>retain-choice</code> comes in: it lets you be useful without being 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.</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.</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 custom workflow around save and restore.</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[<p>Few things are more annoying than losing your progress halfway through a form. Maybe the browser crashes. Maybe the tab closes. Maybe your kid yells from the other room, and you come back three hours later wondering why you 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 painful, which helps because forms are usually frustrating enough on their own.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Few things are more annoying than losing your progress halfway through a form. Maybe the browser crashes. Maybe the tab closes. Maybe your kid yells from the other room, and you come back three hours later wondering why you 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 painful, which helps because forms are usually frustrating enough on their own.</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="user experience" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/never-lose-form-progress-again.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/repeatable-form-fields-made-simple/</id><title type="html"><![CDATA[✍🏻 Repeatable Form Fields Made Simple]]></title><link href="https://www.aaron-gustafson.com/notebook/repeatable-form-fields-made-simple/" rel="alternate" type="text/html" /><published>2026-01-31T00:04:36Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Sometimes you need people to provide multiple instances of the same information: email addresses, phone numbers, team members, or emergency contacts. The <code>form-repeatable</code> web component handles that cleanly, including duplication, renumbering, and seamless form submission via the ElementInternals API.</p><p>All you need to do is provide a single field group and the component handles the rest:</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>form-repeatable</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>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>stop-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Stop 1<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>stop-1<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>text<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>stops[]<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 punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</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>The <code>form-repeatable</code> component treats its first child as a template and injects a <code>button</code> that allows users to repeat the field. When users click “Add Another” (the default “add” button text), the following happens:</p><ol><li>The template is cloned,</li><li>Any numbers are auto-incremented (“Stop 1” → “Stop 2”, <code>stop-1</code> → <code>stop-2</code>),</li><li>A new group is added to the component,</li><li>A “remove” button is added when there’s more than the minimum number of groups (1 by default), and</li><li>Form values update automatically via <code>ElementInternals</code>.</li></ol><p>That last piece is crucial. The plugin is a fully-participating member in the parent form:</p><ul><li>All inputs are collected and submitted automatically</li><li>Values are added to <code>FormData</code></li><li>In-built form reset is respected</li><li>A form’s disabled state is respected</li></ul><p>No special handling required; it works like a native form control.</p><h2 id="need-customized-buttons%3F-you-bet!" tabindex="-1"><a class="header-anchor" href="#need-customized-buttons%3F-you-bet!" aria-hidden="true">#</a> Need customized buttons? You bet!</h2><p>If you don’t like the default text, or your site is in another language, you can define your own button labels using the <code>add-label</code> and <code>remove-label</code> attributes:</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-repeatable</span><span class="token attr-name">add-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Add Another Item<span class="token punctuation">”</span></span><span class="token attr-name">remove-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Delete<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 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>item-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Item 1<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>item-1<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>text<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>items[]<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 punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>With that simple change, the add button reads “Add Another Item” and each remove button reads “Delete”. To improve the experience for screen reader users, the <code>remove-label</code> value is combined with the associated label/legend to create accessible names like “Delete Item 1” which is far more helpful.</p><h2 id="already-have-values-to-show%3F-no-problem." tabindex="-1"><a class="header-anchor" href="#already-have-values-to-show%3F-no-problem." aria-hidden="true">#</a> Already have values to show? No problem.</h2><p>If your form needs to start with multiple groups already filled in, just provide them as child elements:</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>form-repeatable</span><span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<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 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>phone-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 1<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-1<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>tel<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>phones[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>555-0100<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 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>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>phone-2<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 2<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-2<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>tel<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>phones[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>555-0101<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 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>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>phone-3<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 3<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-3<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>tel<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>phones[]<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 punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</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>All child elements become managed groups, and existing values are preserved. Perfect progressive enhancement!</p><h2 id="need-to-do-something-more-complex%3F-i-got-you." tabindex="-1"><a class="header-anchor" href="#need-to-do-something-more-complex%3F-i-got-you." aria-hidden="true">#</a> Need to do something more complex? I got you.</h2><p>You’re not limited to repeating a single field. Each group can contain multiple, related fields. Here’s an example with a <code>fieldset</code> for guest information:</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-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Guest 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</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>guest-name-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Name<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-name-1<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>text<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>guest-name-1<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Email<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<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">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<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>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>When the component initializes, the whole <code>fieldset</code> becomes the template. As users add guests, numeric values in text and attributes increment automatically. In this example, that means the <code>legend</code>, <code>for</code>, <code>id</code>, and <code>name</code> values all stay in sync.</p><h2 id="need-to-constrain-the-responses%3F-sure-thing." tabindex="-1"><a class="header-anchor" href="#need-to-constrain-the-responses%3F-sure-thing." aria-hidden="true">#</a> Need to constrain the responses? Sure thing.</h2><p>Use the <code>min</code> and <code>max</code> attributes to control the number of allowed groups:</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-repeatable</span><span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<span class="token punctuation">”</span></span><span class="token attr-name">max</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 attr-name">add-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Add Team Member<span class="token punctuation">”</span></span><span class="token attr-name">remove-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Remove<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 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>member-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Team Member 1<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>member-1<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>text<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>members[]<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 punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>This creates a component that:</p><ul><li>Starts with 1 member,</li><li>Requires adding new members until the <code>min</code> threshold (2) is met,</li><li>Cannot have fewer than 2 team members,</li><li>Cannot have more than 5 team members, and</li><li>Uses custom button labels.</li></ul><p>The remove buttons are not shown when at the minimum threshold (1 by default) and the add button disappears when you hit the maximum.</p><h2 id="prefer-an-explicit-template%3F-bring-it!" tabindex="-1"><a class="header-anchor" href="#prefer-an-explicit-template%3F-bring-it!" aria-hidden="true">#</a> Prefer an explicit <code>template</code>? Bring it!</h2><p>This component can accept a <code>template</code> element containing the fields you want to repeat. Just drop in <code>{n}</code> placeholders where you want the sequential numbers to appear:</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-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</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>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>email-{n}<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Email {n}<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email-{n}<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">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>emails[]<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 punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>When hoisted into the component, the <code>template</code> element is removed from the light DOM and used internally.</p><h2 id="here%E2%80%99s-what-you-need-to-know-about-styling-it" tabindex="-1"><a class="header-anchor" href="#here%E2%80%99s-what-you-need-to-know-about-styling-it" aria-hidden="true">#</a> Here’s what you need to know about styling it</h2><p>The component uses Shadow DOM to encapsulate its internal structure, but you can style it using CSS parts and custom properties. It also adopts your global styles automatically.</p><p>The component uses CSS Grid by default:</p><ul><li><strong>Two columns</strong>: Content in column 1, remove buttons aligned inline end in column 2</li><li><strong>Subgrid</strong>: Each group uses <code>subgrid</code> to align with parent grid</li><li><strong>Add button</strong>: Appears below all groups</li></ul><p>You can use CSS parts to style the buttons and field groups. Here are some examples:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token comment">/* Style all buttons <em>/</span><span class="token selector">form-repeatable::part(button)</span><span class="token punctuation">{</span><span class="token property">padding</span><span class="token punctuation">:</span> 0.5rem 1rem<span class="token punctuation">;</span><span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span><span class="token property">cursor</span><span class="token punctuation">:</span> pointer<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style the add button <em>/</span><span class="token selector">form-repeatable::part(add-button)</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #28a745<span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style remove buttons <em>/</span><span class="token selector">form-repeatable::part(remove-button)</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #dc3545<span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Customize the grid layout <em>/</span><span class="token selector">form-repeatable::part(groups)</span><span class="token punctuation">{</span><span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 1fr auto<span class="token punctuation">;</span><span class="token property">gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style each group */</span><span class="token selector">form-repeatable::part(group)</span><span class="token punctuation">{</span><span class="token property">padding</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token property">background</span><span class="token punctuation">:</span> #f8f9fa<span class="token punctuation">;</span><span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span><span class="token property">margin-bottom</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>Available parts:</p><ul><li><code>groups</code> - Container for all groups (CSS grid by default)</li><li><code>group</code> - Each repeatable group wrapper</li><li><code>content</code> - Container for group’s fields</li><li><code>group-controls</code> - Container for the remove button</li><li><code>controls</code> - Container for the add button</li><li><code>button</code> - All buttons</li><li><code>add-button</code> - The add button</li><li><code>remove-button</code> - All remove buttons</li></ul><h2 id="want-to-hook-into-changes%3F-you%E2%80%99re-extra%2C-but-sure." tabindex="-1"><a class="header-anchor" href="#want-to-hook-into-changes%3F-you%E2%80%99re-extra%2C-but-sure." aria-hidden="true">#</a> Want to hook into changes? You’re extra, but sure.</h2><p>You can listen for when groups are added or removed and run your own custom code:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> repeatable <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-repeatable”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>repeatable<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-repeatable:added”</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>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Group added. Total groups:”</span><span class="token punctuation">,</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>groupCount<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>repeatable<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-repeatable:removed”</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>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Group removed. Total groups:”</span><span class="token punctuation">,</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>groupCount<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="go-with-the-progressive-enhancement-flow" tabindex="-1"><a class="header-anchor" href="#go-with-the-progressive-enhancement-flow" aria-hidden="true">#</a> Go with the progressive enhancement flow</h2><p>If JavaScript fails, users still see the initial field group(s) and can complete the form. They won’t be able to add more, but nothing breaks. Set your minimums accordingly.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>Explore <a href="https://aarongustafson.github.io/form-repeatable/demo/">the demo</a> with various examples:</p><figure id="fig-2025-12-06-07" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-repeatable/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it-now" tabindex="-1"><a class="header-anchor" href="#grab-it-now" aria-hidden="true">#</a> Grab it now</h2><p>Check out the project on <a href="https://github.com/aarongustafson/form-repeatable">GitHub</a>. Install via 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-repeatable</code></pre><p>Import and go:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/form-repeatable”</span><span class="token punctuation">;</span></code></pre><p>This single component manages repeatable field groups with native form participation, no framework required.</p>]]></content><amg:twitter><![CDATA[Add repeatable form field groups with automatic numbering and native form participation.]]></amg:twitter><amg:summary><![CDATA[<p>Sometimes you need people to provide multiple instances of the same information: email addresses, phone numbers, team members, or emergency contacts. The <code>form-repeatable</code> web component handles that cleanly, including duplication, renumbering, and seamless form submission via the ElementInternals API.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Sometimes you need people to provide multiple instances of the same information: email addresses, phone numbers, team members, or emergency contacts. The <code>form-repeatable</code> web component handles that cleanly, including duplication, renumbering, and seamless form submission via the ElementInternals API.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/repeatable-form-fields-made-simple.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/dynamic-datalist-autocomplete-from-an-api/</id><title type="html"><![CDATA[✍🏻 Dynamic Datalist: Autocomplete from an API]]></title><link href="https://www.aaron-gustafson.com/notebook/dynamic-datalist-autocomplete-from-an-api/" rel="alternate" type="text/html" /><published>2025-12-16T19:46:29Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>HTML’s <code>datalist</code> element provides native autocomplete functionality, but it’s entirely static—you have to know all the options up front. The <code>dynamic-datalist</code> web component solves this by fetching suggestions from an API endpoint as users type, giving you the benefits of native autocomplete with the flexibility of dynamic data.</p><p>This component is a modern replacement for <a href="https://github.com/easy-designs/jquery.easy-predictive-typing.js">my old jQuery predictive typing plugin</a>. I’ve reimagined it as a standards-based web component.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>To use the component, wrap it around your <code>input</code> field and specify an endpoint:</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>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/search<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Search<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>search<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>search<span class="token punctuation">”</span></span><span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Type to search…<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>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>As users type, the component makes GET requests to that endpoint, passing in the typed value as the “query” parameter (e.g., <code>/api/search?query=WHAT_THE_USER_TYPED</code>). The response from the endpoint is used to populates a dynamic <code>datalist</code> element with the results.</p><p>The structure of the response should be JSON with an <code>options</code> array of string values:</p><pre class="language-json" tabindex="0"><code class="language-json"><span class="token punctuation">{</span><span class="token property">“options”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token string">“option 1”</span><span class="token punctuation">,</span><span class="token string">“option 2”</span><span class="token punctuation">,</span><span class="token string">“option 3”</span><span class="token punctuation">]</span><span class="token punctuation">}</span></code></pre><h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works" aria-hidden="true">#</a> How it works</h2><p>Under the hood, the component:</p><ol><li>Adopts (or creates) a <code>datalist</code> element for your <code>input</code>,</li><li>Listens for “input” events,</li><li>Debounces requests (waiting at least 250ms) to avoid overwhelming your API,</li><li>Sends requests to your endpoint with the current value of the <code>input</code>,</li><li>Reads back the JSON response,</li><li>Updates the <code>datalist</code><code>option</code> elements, and</li><li>Dispatches the update event.</li></ol><p>All of this happens transparently—users just see autocomplete suggestions appearing as they type.</p><h2 id="need-post%3F" tabindex="-1"><a class="header-anchor" href="#need-post%3F" aria-hidden="true">#</a> Need POST?</h2><p>You can change the submission method via the <code>method</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>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/lookup<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>lookup<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Lookup<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>lookup<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>lookup<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>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>This sends a POST request with a JSON body: <code>{ &quot;query&quot;: &quot;…&quot; }</code>. Currently GET and POST are supported, but I could add more if folks want them.</p><h2 id="custom-variable-names" tabindex="-1"><a class="header-anchor" href="#custom-variable-names" aria-hidden="true">#</a> Custom variable names</h2><p>As I mentioned, the component uses “query” as the parameter name by default, but you can easily change it via the <code>key</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>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/terms<span class="token punctuation">”</span></span><span class="token attr-name">key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>term<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Term search<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>search<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>term<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>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>This sends the GET request <code>/api/terms?term=…</code>.</p><h2 id="working-with-existing-datalists" tabindex="-1"><a class="header-anchor" href="#working-with-existing-datalists" aria-hidden="true">#</a> Working with existing datalists</h2><p>If your <code>input</code> already has a <code>datalist</code> defined, the component will inherit it and replace the existing options with the fetched results, which makes for a nice progressive enhancement:</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>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/cities<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>city<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>City<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>city<span class="token punctuation">”</span></span><span class="token attr-name">list</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>cities-list<span class="token punctuation">”</span></span><span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Type a city…<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>datalist</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>cities-list<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>option</span><span class="token punctuation">&gt;</span></span>New York<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>Los Angeles<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>Chicago<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>datalist</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>Users see the pre-populated cities immediately, and as they type, API results supplement the list. If JavaScript fails or the web component doesn’t load, users still get the static options. Nothing breaks.</p><h2 id="event-handling" tabindex="-1"><a class="header-anchor" href="#event-handling" aria-hidden="true">#</a> Event handling</h2><p>If you want to tap into the component’s event system, it fires three custom events:</p><ul><li><code>dynamic-datalist:ready</code> - Fired when the component initializes</li><li><code>dynamic-datalist:update</code> - Fired when the <code>datalist</code> is updated with new options</li><li><code>dynamic-datalist:error</code> - Fired when an error occurs fetching data</li></ul><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> element <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">“dynamic-datalist”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:ready”</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>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Component ready:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<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>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:update”</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>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Options updated:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>options<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>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:error”</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>console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">“Error:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>error<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>Each event provides helpful <code>detail</code> objects with references to the <code>input</code>, <code>datalist</code>, and other relevant data.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>Check out <a href="https://aarongustafson.github.io/dynamic-datalist/demo/">the demo</a> for live examples (there are also <a href="https://aarongustafson.github.io/dynamic-datalist/demo/unpkg.html">unpkg</a> and <a href="https://aarongustafson.github.io/dynamic-datalist/demo/esm.html">ESM</a> builds if you want to test CDN delivery):</p><figure id="fig-2025-12-06-03" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/dynamic-datalist/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/dynamic-datalist">GitHub</a>. You can also install via 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/dynamic-datalist</code></pre><p>If you go that route, there are a few ways to register the element depending on your build setup:</p><h3 id="option-1%3A-define-it-yourself" tabindex="-1"><a class="header-anchor" href="#option-1%3A-define-it-yourself" aria-hidden="true">#</a> Option 1: Define it yourself</h3><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> DynamicDatalistElement <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/dynamic-datalist”</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">“dynamic-datalist”</span><span class="token punctuation">,</span> DynamicDatalistElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="option-2%3A-let-the-helper-guard-registration" tabindex="-1"><a class="header-anchor" href="#option-2%3A-let-the-helper-guard-registration" aria-hidden="true">#</a> Option 2: Let the helper guard registration</h3><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/dynamic-datalist/define.js”</span><span class="token punctuation">;</span><span class="token comment">// or, when you need to wait:</span><span class="token keyword">import</span><span class="token punctuation">{</span> defineDynamicDatalist <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/dynamic-datalist/define.js”</span><span class="token punctuation">;</span><span class="token function">defineDynamicDatalist</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="option-3%3A-drop-the-helper-in-via-a-%3Cscript%3E-tag" tabindex="-1"><a class="header-anchor" href="#option-3%3A-drop-the-helper-in-via-a-%3Cscript%3E-tag" aria-hidden="true">#</a> Option 3: Drop the helper in via a <code>&lt;script&gt;</code> tag</h3><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>./node_modules/@aarongustafson/dynamic-datalist/define.js<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>module<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span></code></pre><p>Regardless of how you register it, there are no framework dependencies—just clean autocomplete powered by your API. As I mentioned, it’s also available via CDNs, such as unpkg too, if you’d prefer to go that route.</p>]]></content><amg:twitter><![CDATA[Want API-driven autocomplete suggestions in your forms? Here’s a web component that makes it happen.]]></amg:twitter><amg:summary><![CDATA[<p>HTML’s <code>datalist</code> element provides native autocomplete functionality, but it’s entirely static—you have to know all the options up front. The <code>dynamic-datalist</code> web component solves this by fetching suggestions from an API endpoint as users type, giving you the benefits of native autocomplete with the flexibility of dynamic data.</p>]]></amg:summary><summary type="html"><![CDATA[<p>HTML’s <code>datalist</code> element provides native autocomplete functionality, but it’s entirely static—you have to know all the options up front. The <code>dynamic-datalist</code> web component solves this by fetching suggestions from an API endpoint as users type, giving you the benefits of native autocomplete with the flexibility of dynamic data.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><category term="API" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/dynamic-datalist-autocomplete-from-an-api.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-for-obfuscating-form-fields/</id><title type="html"><![CDATA[✍🏻 A Web Component for Obfuscating Form Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-for-obfuscating-form-fields/" rel="alternate" type="text/html" /><published>2025-12-06T20:03:47Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>We have the password reveal pattern for passwords, but what about other sensitive fields that need to be readable while editing and obfuscated while at rest? The <code>form-obfuscator</code> web component does exactly that.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>Wrap any text field in the component and it will automatically obfuscate the value when the field loses focus:</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-obfuscator</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>secret-key-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>What was your first pet’s name?<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>secret-key-1<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>secret-key-1<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-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>When users click into the field, they see the actual value. When they click away, it’s replaced with asterisks (<em>). The real value is preserved in a hidden field for form submission.</p><h2 id="custom-obfuscation-characters" tabindex="-1"><a class="header-anchor" href="#custom-obfuscation-characters" aria-hidden="true">#</a> Custom obfuscation characters</h2><p>If you don’t like asterisks, you can specify any character you like:</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-obfuscator</span><span class="token attr-name">character</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><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>account<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Account Number<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>account<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>account<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-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>Or get creative:</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-obfuscator</span><span class="token attr-name">character</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><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>ssn<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Social Security Number<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>ssn<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>ssn<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-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><h2 id="pattern-based-obfuscation" tabindex="-1"><a class="header-anchor" href="#pattern-based-obfuscation" aria-hidden="true">#</a> Pattern-based obfuscation</h2><p>Sometimes you want to show part of the value while hiding the rest. The <code>pattern</code> attribute lets you specify which characters to keep visible:</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-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>\d{4}$<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Social Security Number<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>ssn<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>ssn<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-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This keeps the last four digits visible while replacing everything else with your obfuscation character. Perfect for Social Security Numbers, credit cards, or phone numbers where showing the last few digits helps users confirm they’ve entered the right value.</p><h2 id="limiting-displayed-characters" tabindex="-1"><a class="header-anchor" href="#limiting-displayed-characters" aria-hidden="true">#</a> Limiting displayed characters</h2><p>Use the <code>maxlength</code> attribute to cap how many characters appear when obfuscated:</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-obfuscator</span><span class="token attr-name">maxlength</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>4<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 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>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>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 punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>Even if the user enters a 20-character value, only four asterisks will be displayed when the field is obfuscated. This prevents giving away information about the length of the information entered.</p><h2 id="custom-replacement-functions" tabindex="-1"><a class="header-anchor" href="#custom-replacement-functions" aria-hidden="true">#</a> Custom replacement functions</h2><p>For complete control, you can provide a JavaScript function via the <code>replacer</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>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript">window<span class="token punctuation">.</span><span class="token function-variable function">emailReplacer</span><span class="token operator">=</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> username <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">var</span> domain <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">return</span> username<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">.</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span><span class="token string">&quot;</em>“</span><span class="token punctuation">)</span><span class="token operator">+</span> domain<span class="token punctuation">;</span><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>script</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>^(.<em>?)(@.+)$<span class="token punctuation">“</span></span><span class="token attr-name">replacer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>return emailReplacer(arguments)<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 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 punctuation">&gt;</span></span>Email Address<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>email<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>email<span class="token punctuation">“</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span><a href="mailto:user@example.com">user@example.com</a><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-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This example uses a pattern to separate the username from the domain, then obfuscates only the username portion, leaving <code>@example.com</code> visible.</p><p>Here’s another practical example for credit cards:</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>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript"><span class="token keyword">function</span><span class="token function">cardNumberReplacer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> beginning <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">var</span> final_digits <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">return</span> beginning<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\d</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span><span class="token string">”</em>“</span><span class="token punctuation">)</span><span class="token operator">+</span> final_digits<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>script</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>^((?:[\d]+-)+)(\d+)$<span class="token punctuation">“</span></span><span class="token attr-name">replacer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>return cardNumberReplacer(arguments)<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>cc<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Credit Card<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>cc<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>cc<span class="token punctuation">“</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>1234-5678-9012-3456<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-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This displays as <code><strong><strong>-</strong></strong>-****-3456</code>, showing only the last group of digits.</p><h2 id="combining-attributes" tabindex="-1"><a class="header-anchor" href="#combining-attributes" aria-hidden="true">#</a> Combining attributes</h2><p>You can combine these attributes for sophisticated obfuscation patterns:</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-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>\d{4}$<span class="token punctuation">“</span></span><span class="token attr-name">character</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 attr-name">maxlength</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>16<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 attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>card<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Credit Card<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>card<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>card<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-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This keeps the last 4 digits visible, uses bullets for obfuscation, and limits the display to 16 characters total.</p><h2 id="event-handling" tabindex="-1"><a class="header-anchor" href="#event-handling" aria-hidden="true">#</a> Event handling</h2><p>The component dispatches custom events when values are hidden or revealed:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> obfuscator <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-obfuscator”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>obfuscator<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator:hide”</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>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Field obfuscated:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>field<span class="token punctuation">.</span>value<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>obfuscator<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator:reveal”</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>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Field revealed:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>field<span class="token punctuation">.</span>value<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>You can access both the visible field and the hidden field through <code>event.detail.field</code> and <code>event.detail.hidden</code> respectively.</p><h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works" aria-hidden="true">#</a> How it works</h2><p>The component creates a hidden <code>input</code> field to store the actual value for form submission. When the visible field loses focus, it:</p><ol><li>Copies the current value to the hidden field</li><li>Applies your obfuscation rules to create the display value</li><li>Updates the visible field with the obfuscated value</li><li>Dispatches the <code>form-obfuscator:hide</code> event</li></ol><p>When the field gains focus, it:</p><ol><li>Restores the real value from the hidden field</li><li>Updates the visible field</li><li>Dispatches the <code>form-obfuscator:reveal</code> event</li></ol><p>The source order ensures the hidden field is the one that gets submitted with the form.</p><h2 id="progressive-enhancement" tabindex="-1"><a class="header-anchor" href="#progressive-enhancement" aria-hidden="true">#</a> Progressive enhancement</h2><p>The component makes no assumptions about your markup—it works with any text-style <code>input</code> element. If JavaScript fails to load, the field behaves like a normal <code>input</code>, which is exactly what you want. Users can still enter and submit values; they just won’t get the obfuscation behavior.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I’ve created <a href="https://aarongustafson.github.io/form-obfuscator/demo/">a comprehensive demo page showing the various configuration options</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-obfuscator/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>Check out the full project over on <a href="https://github.com/aarongustafson/form-obfuscator">GitHub</a> or 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-obfuscator</code></pre><p>Import and use:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">”@aarongustafson/form-obfuscator&quot;</span><span class="token punctuation">;</span></code></pre><p>No dependencies, just a straightforward way to add field obfuscation to your forms.</p>]]></content><amg:twitter><![CDATA[Need to obfuscate form field values when they’re not being edited? Here’s a web component for that.]]></amg:twitter><amg:summary><![CDATA[<p>We have the password reveal pattern for passwords, but what about other sensitive fields that need to be readable while editing and obfuscated while at rest? The <code>form-obfuscator</code> web component does exactly that.</p>]]></amg:summary><summary type="html"><![CDATA[<p>We have the password reveal pattern for passwords, but what about other sensitive fields that need to be readable while editing and obfuscated while at rest? The <code>form-obfuscator</code> web component does exactly that.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><category term="security" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/a-web-component-for-obfuscating-form-fields.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-for-conditional-dependent-fields/</id><title type="html"><![CDATA[✍🏻 A Web Component for Conditional Dependent Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-for-conditional-dependent-fields/" rel="alternate" type="text/html" /><published>2024-08-14T03:26:03Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>A few weeks back I released <a href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/">a web component to enable you to add requirement rules to checkbox groups</a>. Continuing in the form utility space, I’ve created a new web component that allows you to make fields required based on the values of other fields: <code>form-required-if</code>.</p><p>The <code>form-required-if</code> web component, which is based on <a href="https://github.com/easy-designs/jquery.easy-dependent-required-fields.js">a jQuery plugin I’d written in 2012</a>, looks like this:</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-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email=<em><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>Required if there’s an email value<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>depends-on-email<span class="token punctuation">&quot;</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>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>You wrap any field (and its <code>label</code>) in the component and then declare the conditions under which it should be required in the <code>conditions</code> attribute.</p><h2 id="defining-the-requirement-conditions" tabindex="-1"><a class="header-anchor" href="#defining-the-requirement-conditions" aria-hidden="true">#</a> Defining the requirement conditions</h2><p>Each condition is a key/value pair where the key aligns to the <code>name</code> of the field you need to observe and the value is the value that could trigger the dependency. If any value should trigger the dependency, you use an asterisk (</em>) as the value. In the example above, the field will become required when any value is assigned to the field matching <code>[name=&quot;email&quot;]</code>.</p><p>This <code>conditions</code> attribute can be populated with as many or as few dependencies as make sense for your use case. Multiple conditions are separated by double vertical pipes (<code>||</code> a.k.a. <em>or</em>) as in this 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>form-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email=<em>||test=3<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>Depends on email or test field<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>depends-on-email-or-test<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>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>Here the field depends on one of the following conditions being true:</p><ol><li>the field matching <code>[name=&quot;email&quot;]</code> has a value, <em>or</em></li><li>the field matching <code>[name=&quot;test&quot;]</code> has a value of “3”</li></ol><p>If the field you reference doesn’t exist, no errors will be thrown, it will just quietly exit.</p><h2 id="visually-indicating-a-field-is-required" tabindex="-1"><a class="header-anchor" href="#visually-indicating-a-field-is-required" aria-hidden="true">#</a> Visually indicating a field is required</h2><p>If you typically use an asterisk or similar to indicate a field is required, this web component can support that through one or both of the following attributes:</p><ul><li><code>indicator</code> - This attribute is where you define the indicator itself. It could be something as simple as a string (e.g., <em>), or even full-blown HTML.</li><li><code>indicator-placement</code> - As you can probably guess, this attribute is used to set the position of the indicator. If you want it at the start of the label text, you give it the value “before.” If you want it after the text, you use “after” or don’t use the attribute at all. Indicators will be placed after the label text by default.</li></ul><p>Here’s an example with a custom indicator that is HTML:</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-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>email=</em><span class="token punctuation">”</span></span><span class="token attr-name">indicator</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>&lt;b&gt;</em>&lt;/b&gt;<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>Depends on 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>dep2<span class="token punctuation">&quot;</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>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>If you don’t include markup in your indicator, it will be automatically wrapped in <code>span</code> when injected into the DOM. The <code>hidden</code> and <code>aria-hidden</code> attributes are used to toggle its visibility, relative to the requirement state of the field.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I put together <a href="https://aarongustafson.github.io/form-required-if/demo/">a comprehensive demo of the web component</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-required-if/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>You can view the entire project (and suggest enhancements) over on <a href="https://github.com/aarongustafson/form-required-if">the <code>form-required-if</code> component’s Github repo</a>.</p>]]></content><amg:twitter><![CDATA[Sometimes you need to require a form field only if another field is filled in or has a specific value. Here’s a web component to enable that.]]></amg:twitter><amg:summary><![CDATA[<p>A few weeks back I released <a href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/">a web component to enable you to add requirement rules to checkbox groups</a>. Continuing in the form utility space, I’ve created a new web component that allows you to make fields required based on the values of other fields: <code>form-required-if</code>.</p>]]></amg:summary><summary type="html"><![CDATA[<p>A few weeks back I released <a href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/">a web component to enable you to add requirement rules to checkbox groups</a>. Continuing in the form utility space, I’ve created a new web component that allows you to make fields required based on the values of other fields: <code>form-required-if</code>.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/a-web-component-for-conditional-dependent-fields.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/</id><title type="html"><![CDATA[✍🏻 Requirement Rules for Checkboxes]]></title><link href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/" rel="alternate" type="text/html" /><published>2024-07-05T21:08:34Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>HTML checkboxes debuted as <a href="https://datatracker.ietf.org/doc/html/rfc1866#section-8.1.2.3">part of HTML 2.0 in 1995</a>. Our ability to mark an individual checkbox as being required became part of the HTML5 spec that published in 2014. A decade later, we can still only make checkboxes required on a case-by-case basis. To overcome this limitation, I had created <a href="https://github.com/easy-designs/easy-checkbox-required.js">a jQuery plugin that allowed me to indicate that a user should choose a specific number of items from within a checkbox group</a>. Yesterday I turned that plugin into a web component: <a href="https://github.com/aarongustafson/form-required-checkboxes"><code>form-required-checkboxes</code></a>.</p><h2 id="markup-assumptions" tabindex="-1"><a class="header-anchor" href="#markup-assumptions" aria-hidden="true">#</a> Markup Assumptions</h2><p>Before I tuck into the details, I’ll start by saying that the web component begins with the assumption that you are following best practices with respect to form markup:</p><ul><li>Your checkbox group should be in a <code>fieldset</code> with a <code>legend</code></li><li>All of the checkbox elements must have the same <code>name</code> (e.g., “foo[]”).</li></ul><p>In other words, they should look something like this:</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>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Group 1 label<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</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 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>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>checkbox<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>foo[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>1<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span>First item<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>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 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>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>checkbox<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>foo[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span>Second item<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>li</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- options continue --&gt;</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>fieldset</span><span class="token punctuation">&gt;</span></span></code></pre><p>To use the web component, you wrap the group in a <code>form-required-checkboxes</code> element and then include the JavaScript to initialize it.</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-required-checkboxes</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Group 1 label<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- etc. --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-checkboxes</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- at the end of your document --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/js/web-components/form-required-checkboxes.js<span class="token punctuation">”</span></span><span class="token attr-name">async</span><span class="token punctuation">&gt;</span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span></code></pre><p>If you’re following right along, there’s an error waiting for you in the <code>console</code> — we need to set the requirement rules.</p><h2 id="the-api" tabindex="-1"><a class="header-anchor" href="#the-api" aria-hidden="true">#</a> The API</h2><p>The <code>form-required-checkboxes</code> element requires at least one attribute to function, but using some of the others you can more fully customize the experience for users:</p><ul><li><code>required</code> - Represents the range of required values. You can set this up in one of three ways depending on your needs:<ul><li>Single number (e.g., 3) requires exactly that number of choices.</li><li>Range (e.g., 3-5) requires a minimum of the first number and a max of the second number be chosen.</li><li>Max (e.g., 0-3) requires a minimum of zero and a max of the second number to be chosen.</li></ul></li><li><code>notice</code> (optional) - This is a string description that explains details of the required value in plain language. If you don’t supply one, the component will create one for you based on the current language (if supported). This description will be added as a <code>small</code> element within the component (as a sibling to the <code>fieldset</code>).</li><li><code>error</code> (optional) - This is a string validation error you’d like to be shown when the validation criteria is not met. If not provided, an appropriate error message will be generated based on the current language (if supported).</li><li><code>lang</code> (optional) - Language code for localized messages (e.g., “en,” “es,” “fr,” “de”). Falls back to the nearest ancestor’s <code>lang</code> attribute or the document language.</li></ul><h2 id="localization" tabindex="-1"><a class="header-anchor" href="#localization" aria-hidden="true">#</a> Localization</h2><p>The component now includes built-in translations for 16 languages: English, Chinese (Mandarin), Hindi, Spanish, French, Arabic, Bengali, Portuguese, Russian, Japanese, German, Punjabi, Javanese, Korean, Vietnamese, and Italian. Messages are automatically generated based on the <code>lang</code> attribute.</p><p>You can use it like this:</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-required-checkboxes</span><span class="token attr-name">required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>3<span class="token punctuation">”</span></span><span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>es<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>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Opciones<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Will display: “Elija 3 de la lista” --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-checkboxes</span><span class="token punctuation">&gt;</span></span></code></pre><p>The component automatically detects the language from the <code>lang</code> attribute on the element itself, the nearest ancestor element, or the document’s <code>lang</code> attribute, falling back to English if none is found.</p><p>You can also register custom translations or override existing ones using the <code>FormRequiredCheckboxesElement.registerTranslations()</code> static method. Regional language codes (e.g., <code>en-US</code>, <code>es-MX</code>) automatically fall back to their base language.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I put together <a href="https://aarongustafson.github.io/form-required-checkboxes/demo/">a comprehensive demo of the web component</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-required-checkboxes/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>You can view the entire project (and suggest enhancements) over on <a href="https://github.com/aarongustafson/form-required-checkboxes">the component’s Github repo</a>.</p>]]></content><amg:twitter><![CDATA[Currently, we can only make checkboxes required or not, individually. In some cases you need to be able to set a specific number of checkboxes that need to be checked. My `form-required-checkboxes` web component enables that.]]></amg:twitter><amg:summary><![CDATA[<p>HTML checkboxes debuted as <a href="https://datatracker.ietf.org/doc/html/rfc1866#section-8.1.2.3">part of HTML 2.0 in 1995</a>. Our ability to mark an individual checkbox as being required became part of the HTML5 spec that published in 2014. A decade later, we can still only make checkboxes required on a case-by-case basis. To overcome this limitation, I had created <a href="https://github.com/easy-designs/easy-checkbox-required.js">a jQuery plugin that allowed me to indicate that a user should choose a specific number of items from within a checkbox group</a>. Yesterday I turned that plugin into a web component: <a href="https://github.com/aarongustafson/form-required-checkboxes"><code>form-required-checkboxes</code></a>.</p>]]></amg:summary><summary type="html"><![CDATA[<p>HTML checkboxes debuted as <a href="https://datatracker.ietf.org/doc/html/rfc1866#section-8.1.2.3">part of HTML 2.0 in 1995</a>. Our ability to mark an individual checkbox as being required became part of the HTML5 spec that published in 2014. A decade later, we can still only make checkboxes required on a case-by-case basis. To overcome this limitation, I had created <a href="https://github.com/easy-designs/easy-checkbox-required.js">a jQuery plugin that allowed me to indicate that a user should choose a specific number of items from within a checkbox group</a>. Yesterday I turned that plugin into a web component: <a href="https://github.com/aarongustafson/form-required-checkboxes"><code>form-required-checkboxes</code></a>.</p>]]></summary><category term="accessibility" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/requirement-rules-for-checkboxes.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/better-form-ux-with-the-css-property-field-sizing-/</id><title type="html"><![CDATA[🔗 Better form UX with the CSS property `field-sizing`]]></title><link href="https://www.aaron-gustafson.com/notebook/links/better-form-ux-with-the-css-property-field-sizing-/" rel="alternate" type="text/html" /><link href="https://blog.stephaniestimac.com/posts/2024/01/css-field-sizing/" rel="related" type="text/html" /><published>2024-04-01T22:21:10Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Form fan that I am, I’m excited to have CSS that enables fields (especially <code>textarea</code>) to grow to accommodate the content someone’s in the process of entering into it.</p><p>I distinctly remember spending a good deal of time putting together a proof-of-concept for Twitter DMs to show how it could be done via JavaScript without killing performance, but this is far more elegant.</p>]]></content><amg:twitter><![CDATA[Form fan that I am, I’m excited to have CSS that enables fields (especially `textarea`) to grow to accommodate the content someone’s entered.]]></amg:twitter><category term="forms" /><category term="CSS" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/assume-the-position-a-labelling-story/</id><title type="html"><![CDATA[🔗 Assume the Position—A Labelling Story]]></title><link href="https://www.aaron-gustafson.com/notebook/links/assume-the-position-a-labelling-story/" rel="alternate" type="text/html" /><link href="https://www.tpgi.com/assume-the-position-a-labelling-story/" rel="related" type="text/html" /><published>2023-06-07T20:40:33Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Interesting examination of label positioning relative to checkboxes and radio controls in forms. While ostensibly web-focused, it applies equally to any GUI.</p>]]></content><amg:twitter><![CDATA[Interesting examination of label positioning relative to checkboxes and radio controls in forms. While ostensibly web-focused, it applies equally to any GUI.]]></amg:twitter><category term="accessibility" /><category term="forms" /><category term="design" /><category term="user experience" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/checkout-usability-autodetect-city-and-state-inputs-based-on-the-user-s-postal-code-28-of-mobile-sites-don-t-/</id><title type="html"><![CDATA[🔗 Checkout Usability: Autodetect “City” and “State” Inputs Based on the User’s Postal Code (28% of Mobile Sites Don’t)]]></title><link href="https://www.aaron-gustafson.com/notebook/links/checkout-usability-autodetect-city-and-state-inputs-based-on-the-user-s-postal-code-28-of-mobile-sites-don-t-/" rel="alternate" type="text/html" /><link href="https://baymard.com/blog/zip-code-auto-detection?utm_medium=email&amp;utm_campaign=Article%20Checkout%20Usability%20Autodetect%20City%20and%20State%20Inputs%20Based%20on%20the%20Users%20Postal%20Code%2028%20of%20Mobile%20Sites%20Dont&amp;utm_content=Article%20Checkout%20Usability%20Autodetect%20City%20and%20State%20Inputs%20Based%20on%20the%20Users%20Postal%20Code%2028%20of%20Mobile%20Sites%20Dont+CID_06e39fd9939f69a009f7b4d63d1e0da8&amp;utm_source=CampaignMonitor&amp;utm_term=read%20the%20article" rel="related" type="text/html" /><published>2023-04-12T23:01:09Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Excellent overview of how to progressively enhance address entry forms using postal codes. It’s both a time saver and a data quality guard. Great stuff!</p>]]></content><amg:twitter><![CDATA[Excellent overview of how to progressively enhance address entry forms using postal codes. It’s both a time saver and a data quality guard. Great stuff!]]></amg:twitter><category term="progressive enhancement" /><category term="forms" /><category term="user experience" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/use-buttons-or-buttons-plus-an-open-text-field-for-updating-cart-quantity/</id><title type="html"><![CDATA[🔗 Use Buttons or Buttons Plus an Open Text Field for Updating Cart Quantity]]></title><link href="https://www.aaron-gustafson.com/notebook/links/use-buttons-or-buttons-plus-an-open-text-field-for-updating-cart-quantity/" rel="alternate" type="text/html" /><link href="https://baymard.com/blog/auto-update-users-quantity-changes" rel="related" type="text/html" /><published>2022-11-05T03:46:22Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I’m living this super deep dive into the UX of the quantity field in e-commerce.</p>]]></content><amg:twitter><![CDATA[Super deep dive into the UX of the quantity field in e-commerce.]]></amg:twitter><category term="forms" /><category term="web design" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/spellcheckers-exfiltrating-pii_-not-so-fast/</id><title type="html"><![CDATA[✍🏻 Spellcheckers exfiltrating PII… not so fast]]></title><link href="https://www.aaron-gustafson.com/notebook/spellcheckers-exfiltrating-pii_-not-so-fast/" rel="alternate" type="text/html" /><published>2022-09-19T21:34:51Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>A recent post from the <a href="https://www.otto-js.com">Otto JS</a> research team highlighted <a href="https://www.otto-js.com/news/article/chrome-and-edge-enhanced-spellcheck-features-expose-pii-even-your-passwords">how spellcheck services can inadvertently exfiltrate sensitive user data, including passwords</a>, from your site. To be honest, I found the post a tad alarmist and lacking when it came to recommending solid protections. Consider this your no-nonsense guide to protecting your users’ sensitive information.</p><h2 id="background" tabindex="-1"><a class="header-anchor" href="#background" aria-hidden="true">#</a> Background</h2><p>In their research, the Otto JS team found that Chrome and Edge each use their own web services to drive their spellchecking services. In Chrome’s case it seems to only be with their “advanced spellcheck” feature turned on. I was able to validate this behavior using Charles on macOS Monterey using the latest Chrome Stable and the latest Edge Canary. I did not see the same exfiltration happen with Safari or Firefox. I created <a href="https://codepen.io/aarongustafson/pen/gOzWNgM">a CodePen form with several fields to test different permutations of form field attributes and behaviors</a>. What I am sharing, below, is a result of that testing.</p><h2 id="what-gets-sent%3F" tabindex="-1"><a class="header-anchor" href="#what-gets-sent%3F" aria-hidden="true">#</a> What gets sent?</h2><p>In both Chrome and Edge, the information sent to their services is the text value itself, disconnected from any specific field name. Chrome also sends the user’s language, and country. Edge, which uses the Microsoft Editor service under the hood, also sends a bunch of licensing details and the user’s language.</p><h2 id="password-fields-are-safe" tabindex="-1"><a class="header-anchor" href="#password-fields-are-safe" aria-hidden="true">#</a> Password fields are safe</h2><pre class="language-html" tabindex="0"><code class="language-html"><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 punctuation">/&gt;</span></span></code></pre><p>Browsers do a lot to protect the contents of password fields already, so I wasn’t surprised to see that the contents of password fields are <strong>not</strong> passed to a spellcheck service.</p><p>If you are not using a true password field, however, the contents of that field are not protected in the same way. If you build a custom password control, you’re on the hook to replicate the entirety of the browser’s feature set when it comes to protecting user data. Unless you’re a glutton for punishment, you should probably just use the built-in password field instead.</p><h2 id="fields-marked-readonly-and-disabled-are-not-exposed" tabindex="-1"><a class="header-anchor" href="#fields-marked-readonly-and-disabled-are-not-exposed" aria-hidden="true">#</a> Fields marked readonly and disabled are not exposed</h2><p>As you’d hope, neither read-only fields nor disabled fields are exposed ot the service. This even holds true when you change the values of these fields programmatically or via DevTools.</p><p>I should note, however, that <code>readonly</code> fields <em>are</em> send to the server when the form is submitted and their contents are editable via JavaScript and DevTools, so you should always assume <code>readonly</code> fields are informational for the user only and never trust their contents on the server side. Fields that are marked <code>disabled</code>, in contrast, are never sent to the server.</p><h2 id="you-can-protect-interactive-fields-with-the-spellcheck-attribute" tabindex="-1"><a class="header-anchor" href="#you-can-protect-interactive-fields-with-the-spellcheck-attribute" aria-hidden="true">#</a> You can protect interactive fields with the <code>spellcheck</code> attribute</h2><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</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>no-spellcheck<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>no-spellcheck<span class="token punctuation">”</span></span><span class="token attr-name">spellcheck</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>false<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span></code></pre><p>The <code>spellcheck</code> attribute can be applied to any element and setting it to “false” instructs browsers to turn off spellchecking services for its contents. The post from Otto JS showed this being used globally on the <code>body</code> element, but that is overkill. It would be better to use the attribute on specific fields you want to protect, as shown above.</p><h2 id="don%E2%80%98t-forget-about-password-controls-that-support-show%2Fhide" tabindex="-1"><a class="header-anchor" href="#don%E2%80%98t-forget-about-password-controls-that-support-show%2Fhide" aria-hidden="true">#</a> Don‘t forget about password controls that support show/hide</h2><p>Edge has a neat feature in its password field implementation where it enables a user to toggle the visibility of the password within the control itself. When users show their password using that built-in functionality, no content is shared with the spellchecker. Not all browsers have this feature, however, which has led to JavaScript-based implementations that simply swap the “password” <code>type</code> value for “text” to show the contents and then swap it back again to hide the contents. Here’s a quick &amp; dirty example of what the toggle button’s event handler might look like:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">function</span><span class="token function">togglePassword</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> $btn <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">,</span>$field <span class="token operator">=</span> $btn<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“input”</span><span class="token punctuation">)</span><span class="token punctuation">,</span>state <span class="token operator">=</span> $field<span class="token punctuation">.</span>type<span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span>$btn <span class="token operator">&amp;&amp;</span> $field<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">if</span><span class="token punctuation">(</span>state <span class="token operator">==</span><span class="token string">“password”</span><span class="token punctuation">)</span><span class="token punctuation">{</span>$field<span class="token punctuation">.</span>type <span class="token operator">=</span><span class="token string">“text”</span><span class="token punctuation">;</span>$btn<span class="token punctuation">.</span>innerText <span class="token operator">=</span><span class="token string">“Hide”</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">else</span><span class="token punctuation">{</span>$field<span class="token punctuation">.</span>type <span class="token operator">=</span><span class="token string">“password”</span><span class="token punctuation">;</span>$btn<span class="token punctuation">.</span>innerText <span class="token operator">=</span><span class="token string">“Show”</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>e<span class="token punctuation">.</span><span class="token function">stopPropagation</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 problem with this approach, when it comes to the spellchecker, is that the text field is considered fair game for checking. Its contents aren’t sent right away, but if the field receives focus or its value is changed in any way while in the “text” state, its contents are sent to the spellcheck service.</p><p>Protecting the field’s contents are fairly easy, however: turn off the spellchecker for that field, even in its “password” state.</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>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-show-nospellcheck<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-show-nospellcheck<span class="token punctuation">”</span></span><span class="token attr-name">spellcheck</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>false<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span></code></pre><p>With <code>spellcheck=&quot;false&quot;</code> in place, you can turn the field into a text field safely, without the contents being exposed to the spellcheck service.</p><hr><p>HTML is a pretty powerful language and, when wielded properly, can be an excellent tool for protecting user data, provided we use it properly. And know that you know how to protect your users’ sensitive data, minimize this tab and start filing those pull requests…</p>]]></content><amg:twitter><![CDATA[A collection of ways you can keep sensitive user data protected from accidental exfiltration.]]></amg:twitter><amg:summary><![CDATA[<p>A recent post from the <a href="https://www.otto-js.com">Otto JS</a> research team highlighted <a href="https://www.otto-js.com/news/article/chrome-and-edge-enhanced-spellcheck-features-expose-pii-even-your-passwords">how spellcheck services can inadvertently exfiltrate sensitive user data, including passwords</a>, from your site. To be honest, I found the post a tad alarmist and lacking when it came to recommending solid protections. Consider this your no-nonsense guide to protecting your users’ sensitive information.</p>]]></amg:summary><summary type="html"><![CDATA[<p>A recent post from the <a href="https://www.otto-js.com">Otto JS</a> research team highlighted <a href="https://www.otto-js.com/news/article/chrome-and-edge-enhanced-spellcheck-features-expose-pii-even-your-passwords">how spellcheck services can inadvertently exfiltrate sensitive user data, including passwords</a>, from your site. To be honest, I found the post a tad alarmist and lacking when it came to recommending solid protections. Consider this your no-nonsense guide to protecting your users’ sensitive information.</p>]]></summary><category term="browsers" /><category term="forms" /><category term="HTML" /><category term="security" /><category term="privacy" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/spellcheckers-exfiltrating-pii_-not-so-fast.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/e-commerce-accessibility-specifying-ui-elements-using-names-/</id><title type="html"><![CDATA[🔗 E-Commerce Accessibility: Specifying UI Elements Using “Names”]]></title><link href="https://www.aaron-gustafson.com/notebook/links/e-commerce-accessibility-specifying-ui-elements-using-names-/" rel="alternate" type="text/html" /><link href="https://baymard.com/blog/names-accessibility" rel="related" type="text/html" /><published>2022-08-30T23:20:35Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<blockquote><p>For users relying on assistive technology such as screen readers, it’s … critically important to have programmatically determined names identifying various UI elements. … Doing so consistently with all interactive page elements will help ensure users using assistive technologies will be able to navigate through a site and complete a purchase successfully.</p></blockquote><p>Could not have said it better myself. This article is chock full of excellent advice, not only on why names are important, but how to ensure your interactive components are properly named.</p>]]></content><amg:twitter><![CDATA[Great article from @Baymard, covering why your interactive components need to be named and a few different ways to do it. Applicable beyond shopping sites too.]]></amg:twitter><category term="accessibility" /><category term="forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/designing-better-error-messages-ux/</id><title type="html"><![CDATA[🔗 Designing Better Error Messages UX]]></title><link href="https://www.aaron-gustafson.com/notebook/links/designing-better-error-messages-ux/" rel="alternate" type="text/html" /><link href="https://www.smashingmagazine.com/2022/08/error-messages-ux-design/" rel="related" type="text/html" /><published>2022-08-30T16:56:41Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>As you’d expect, Vitaly’s deep dive into error message UX is a treasure trove of excellent, practical advice to make data entry better for your customers.</p>]]></content><amg:twitter><![CDATA[Tons of great info in this @smashingmag piece on the UX of error messages.]]></amg:twitter><category term="forms" /><category term="user experience" /><category term="accessibility" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/bring-focus-to-the-first-form-field-with-an-error/</id><title type="html"><![CDATA[✍🏻 Bring Focus to the First Form Field with an Error]]></title><link href="https://www.aaron-gustafson.com/notebook/bring-focus-to-the-first-form-field-with-an-error/" rel="alternate" type="text/html" /><published>2022-08-22T21:16:02Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>While filling out a long form the other day, I couldn’t figure out why it wasn’t submitting. Turns out I’d forgotten to fill in a field, but I didn’t know that because it had scrolled out of the viewport. This is a common problem on the web, but easily remedied with a little bit of JavaScript.</p><h2 id="step-1%3A-make-the-most-of-your-markup" tabindex="-1"><a class="header-anchor" href="#step-1%3A-make-the-most-of-your-markup" aria-hidden="true">#</a> Step 1: Make the Most of Your Markup</h2><p>Whenever you’re building a form, you should use every tool in your toolbox for building that form properly. That means:</p><ol><li><a href="/notebook/labeled-with-love/">Associate labels with their fields</a></li><li>Use specialized <code>input</code> types (e.g., email, URL) as appropriate</li><li>Identify required fields with <code>required</code> and <code>aria-required</code></li><li>If you’re expecting a field to match a particular format, note that with <code>pattern</code> and provide an example with <code>placeholder</code></li><li>Additional information about a field should be bound to it using <code>aria-describedby</code></li></ol><p>By following these steps, you’re likely already in a good place when it comes to your form’s UX and accessibility. Browsers will pay attention to the instructions in the markup, validate the user’s input, and even provide guidance on how to correct issues without you having to lift a finger. They’ll even focus the first invalid field on your behalf!</p><p>But what if you want to customize the experience?</p><h2 id="step-2%3A-enhanced-browser-validation" tabindex="-1"><a class="header-anchor" href="#step-2%3A-enhanced-browser-validation" aria-hidden="true">#</a> Step 2: Enhanced Browser Validation</h2><p>When you use proper markup for your forms, enhancing them with JavaScript becomes straightforward, thanks to the <a href="https://developer.mozilla.org/docs/Web/API/Constraint_validation">Constraint Validation API</a>. Consider the following form field:</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>input</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>name<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>name<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token attr-name">aria-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>true<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span></code></pre><p>Using JavaScript, we can check to see it’s validity at any point:</p><pre class="language-js" tabindex="0"><code class="language-js">document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">“name”</span><span class="token punctuation">)</span><span class="token punctuation">.</span>validity<span class="token punctuation">.</span>valid<span class="token punctuation">;</span><span class="token comment">// Is either true or false</span></code></pre><p>There are also a host of other properties on the field’s <code>validity</code> property that provide even more detail as to the state of the field’s validity (hence the name: <a href="https://developer.mozilla.org/docs/Web/API/ValidityState"><code>ValidityState</code></a>). In the case of the field above, if <code>valueMissing</code> is <code>true</code> (relating back to that <code>required</code> attribute), <code>valid</code> is <code>false</code>.</p><p>Armed with that knowledge, you could use another feature of the <a href="https://developer.mozilla.org/docs/Web/API/Constraint_validation">Constraint Validation API</a> to show a custom error message in the browser UI:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">var</span> input <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">“name”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>input<span class="token punctuation">.</span>validity<span class="token punctuation">.</span>valid<span class="token punctuation">)</span><span class="token punctuation">{</span>input<span class="token punctuation">.</span><span class="token function">setCustomValidity</span><span class="token punctuation">(</span><span class="token string">“Please enter your name”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>input<span class="token punctuation">.</span><span class="token function">reportValidity</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 thing is, you don’t really want to litter your JavaScript with strings like that. It’s not maintainable. Thankfully, we can leverage markup to achieve the same goal:</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>input</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>name<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>name<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token attr-name">aria-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>true<span class="token punctuation">”</span></span><span class="token attr-name">data-error-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Please enter your name<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span></code></pre><p>The <code>input</code> in this example has a <a href="https://developer.mozilla.org/docs/Learn/HTML/Howto/Use_data_attributes">data attribute</a> containing the error string (<code>data-error-required</code>). We can create whatever data attributes we want and access them using that element’s <code>dataset</code> property. It’s worth noting that hyphenated property names become camelCase when accessed as a named property of <code>dataset</code>.</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">var</span> input <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">“name”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>input<span class="token punctuation">.</span>validity<span class="token punctuation">.</span>valid<span class="token punctuation">)</span><span class="token punctuation">{</span>input<span class="token punctuation">.</span><span class="token function">setCustomValidity</span><span class="token punctuation">(</span>input<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>errorRequired<span class="token punctuation">)</span><span class="token punctuation">;</span>input<span class="token punctuation">.</span><span class="token function">reportValidity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>With that, we get the same result with far looser coupling between individual fields and the JavaScript handling validation.</p><figure id="fig-2022-08-22-01" class="media-container"><p><img src="https://www.aaron-gustafson.com/i/posts/2022-08-22/01.jpg" alt="This custom validation string as rendered in the error tooltip in Safari 15.6"></p></figure><p>You can even extend the approach to handle different kinds of errors:</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>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>email<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>email<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>email<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token attr-name">aria-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>true<span class="token punctuation">”</span></span><span class="token attr-name">data-error-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Please enter your email<span class="token punctuation">”</span></span><span class="token attr-name">data-error-invalid</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Your email doesn’t look right<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span></code></pre><p>Here I’m using two different data attributes to apply in different error scenarios and the one I show will depend on the type of error a user has encountered. Pretty cool!</p><p>And since we’re still operating in the context of the browser’s built-in validation experience, users will still get directed to the first field with an error when they submit the form.</p><p>But what if you want to bypass the built-in experience and go full custom?</p><h2 id="step-3%3A-going-rogue" tabindex="-1"><a class="header-anchor" href="#step-3%3A-going-rogue" aria-hidden="true">#</a> Step 3: Going Rogue</h2><p>The built-in browser validation UI is pretty great, but maybe it’s not your cup of tea. Thankfully, it’s pretty easy to take the training wheels off. You just add a <code>novalidate</code> attribute to the <code>form</code> element. But before you go dropping that attribute in your markup, consider that your custom validation code will all be JavaScript and if that fails to run—<a href="/notebook/progressive-misconceptions/">yes, it happens</a>—the browser won’t step in and fill the void. So instead of putting it in your markup, inject it into the <code>form</code> when you know absolutely all of the JavaScript code necessary to run your custom validation experience is loaded and ready to rock. In fact, it should be the last line of code to execute:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token comment">// Validation Logic Definition</span><span class="token comment">// including validateMe() function</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</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 function">forEach</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">$form</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> validateMe<span class="token punctuation">,</span><span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span>$form<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">“novalidate”</span><span class="token punctuation">,</span><span class="token string">“”</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>With that in place, we can turn our attention to handling the validation setup.</p><p>For simplicity, and based on personal preference, I am going to start with setting up my form to validate when the form is submitted rather than whenever an individual field is changed (hence the “submit” event listener). When the event is fired, the event handler will loop through the fields in the form and validate each one. For the sake of the widest possible browser compatibility, I’m foregoing fat arrow functions and other ES2015 goodies and rockin’ this old school.</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">function</span><span class="token function">validateMe</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> $form <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">,</span>i <span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">,</span>field_count <span class="token operator">=</span> $form<span class="token punctuation">.</span>elements<span class="token punctuation">.</span>length<span class="token punctuation">,</span>$first_error <span class="token operator">=</span><span class="token boolean">false</span><span class="token punctuation">;</span><span class="token keyword">for</span><span class="token punctuation">(</span>i<span class="token punctuation">;</span> i <span class="token operator">&lt;</span> field_count<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> $field <span class="token operator">=</span> $form<span class="token punctuation">.</span>elements<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">,</span>valid <span class="token operator">=</span><span class="token function">isValid</span><span class="token punctuation">(</span>$field<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>$first_error <span class="token operator">&amp;&amp;</span><span class="token operator">!</span>valid<span class="token punctuation">)</span><span class="token punctuation">{</span>$first_error <span class="token operator">=</span> $field<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">if</span><span class="token punctuation">(</span>$first_error<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>$first_error<span class="token punctuation">.</span><span class="token function">focus</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>First, I get a reference to the form that needs to be validated, then I set up the loop for each of the fields. I also set up a reference to the first field with an error—that is the point of the article after all—as <var>$first_error</var> and set it to <code>false</code>.</p><p>Within the loop, I grab a reference to the field and pipe it into another function that will do the actual validation (<code>isValid()</code>). That function will return <code>true</code> if the field is valid and <code>false</code> if it isn’t. The last bit of the loop checks to see if <var>$first_error</var> has been set already and, if it hasn’t, sets it to the current field if it isn’t valid.</p><p>Finally, I check to see if <var>$first_error</var> is set (i.e., true) and if it is, the form has an issue, so I prevent the default behavior of the submit event (using <code>preventDefault()</code>) and focus the field captured in <var>$first_error</var>.</p><h2 id="addendum%3A-associating-custom-error-messages" tabindex="-1"><a class="header-anchor" href="#addendum%3A-associating-custom-error-messages" aria-hidden="true">#</a> Addendum: Associating Custom Error Messages</h2><p>I don’t want to get too into the weeds with my <code>isValid()</code> method, as you can see the end result at the bottom of this post, but I do want to take a moment to describe how you should be handling inline field errors.</p><p>First off, you will need to add <code>aria-invalid=&quot;true&quot;</code> to any fields that have an error. Secondly, you will want to inject the error message into the markup, give its container an <code>id</code>, and <a href="https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Attributes/aria-errormessage">associate it with the field using <code>aria-errormessage</code></a>. So when your JavaScript is done with it, your field will look something like this:</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>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>name<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>What’s Your Name?<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">id</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">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">required</span><span class="token attr-name">aria-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>true<span class="token punctuation">”</span></span><span class="token attr-name">data-error-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Please enter your name<span class="token punctuation">”</span></span><span class="token attr-name">aria-errormessage</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name-validation-error<span class="token punctuation">”</span></span><span class="token attr-name">aria-invalid</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>true<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>strong</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-validation-error<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>name-validation-error<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Please enter your name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>strong</span><span class="token punctuation">&gt;</span></span></code></pre><p>It’s also worth noting that you’ll want to reset and re-validate the form field once the user has changed the contents of the field (and hopefully remediated the error). Resetting the field will involve removing the <code>aria-invalid</code> attribute and doing one of two things with the error message: either remove it from the markup entirely or simply hide it (e.g., <code>display: none</code>). Once the <code>aria-invalid</code> state is reset, it doesn’t matter that there is an existing <code>aria-errormessage</code> attribute. That said, I generally prefer to totally reset the field entirely, removing both attributes and the validation error element as well.</p><p>Here you can see <a href="https://codepen.io/aarongustafson/pen/eYMXMPP">a complete working prototype of this approach</a>:</p><iframe class="codepen" height="400" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/eYMXMPP?height=400&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/aarongustafson/pen/eYMXMPP" target="_blank" rel="noopener">See the Pen</a></p></iframe>Hopefully this helps you with the development of your own forms. Good luck!]]></content><amg:twitter><![CDATA[When highlighting form errors, moving focus to the first error encountered will ensure the error is seen and remedied.]]></amg:twitter><amg:summary><![CDATA[<p>While filling out a long form the other day, I couldn’t figure out why it wasn’t submitting. Turns out I’d forgotten to fill in a field, but I didn’t know that because it had scrolled out of the viewport. This is a common problem on the web, but easily remedied with a little bit of JavaScript.</p>]]></amg:summary><summary type="html"><![CDATA[<p>While filling out a long form the other day, I couldn’t figure out why it wasn’t submitting. Turns out I’d forgotten to fill in a field, but I didn’t know that because it had scrolled out of the viewport. This is a common problem on the web, but easily remedied with a little bit of JavaScript.</p>]]></summary><category term="accessibility" /><category term="forms" /><category term="HTML" /><category term="inclusive design" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="WAI-ARIA" /><category term="web design" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/share-cards/bring-focus-to-the-first-form-field-with-an-error.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/an-html-element-potentially-worth-18m-to-indiegogo-campaigns-adrian-roselli/</id><title type="html"><![CDATA[🔗 An HTML Element Potentially Worth $18M to Indiegogo Campaigns]]></title><link href="https://www.aaron-gustafson.com/notebook/links/an-html-element-potentially-worth-18m-to-indiegogo-campaigns-adrian-roselli/" rel="alternate" type="text/html" /><link href="https://adrianroselli.com/2019/09/an-html-element-potentially-worth-18m-to-indiegogo-campaigns.html" rel="related" type="text/html" /><published>2019-09-25T21:51:16Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Following up on Jason’s post, Adrian offers some forms advice to Indiegogo.</p><blockquote><p>It was immediately apparent that the fields had no accessible name. Despite the visible text label, nothing was programmatically associated.</p></blockquote><p>C’mon people. It’s 2019. Use a fucking <code>label</code> and associate it with the field using the <code>for</code> attribute. If you can figure out React (or Angular or whatever), you can absolutely figure this out.</p>]]></content><amg:twitter><![CDATA[C’mon people. It’s 2019. Use a fucking `label` and associate it with the field using the `for` attribute. If you can figure out React (or Angular or whatever), you can absolutely figure this out.]]></amg:twitter><category term="HTML" /><category term="accessibility" /><category term="forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/an-html-attribute-potentially-worth-4-4m-to-chipotle/</id><title type="html"><![CDATA[🔗 An HTML attribute potentially worth $4.4M to Chipotle]]></title><link href="https://www.aaron-gustafson.com/notebook/links/an-html-attribute-potentially-worth-4-4m-to-chipotle/" rel="alternate" type="text/html" /><link href="https://cloudfour.com/thinks/an-html-attribute-potentially-worth-4-4m-to-chipotle/" rel="related" type="text/html" /><published>2019-09-25T21:48:57Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Excellent forensic investigation from Jason. Reminds me of the kind of stuff we used to do over on <a href="https://webstandardssherpa.com/">Web Standards Sherpa</a>. You should absolutely read the whole thing, but I’ll give you the key takeaways here as your site may be making the same mistake as Chipotle.</p><blockquote><p>There are three key takeaways from Chipotle’s order form that you should consider:</p><ol><li>Use HTML5 input features</li><li>Support autofill</li><li>Make autofill part of your test plans</li></ol></blockquote>]]></content><amg:twitter><![CDATA[Excellent forensic investigation from Jason. Reminds me of the kind of stuff we used to do over on @StandardsSherpa.]]></amg:twitter><category term="HTML" /><category term="JavaScript" /><category term="web development" /><category term="forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/unbutton-buttons/</id><title type="html"><![CDATA[🔗 Unbuttoning Buttons]]></title><link href="https://www.aaron-gustafson.com/notebook/links/unbutton-buttons/" rel="alternate" type="text/html" /><link href="https://www.scottohara.me/blog/2018/10/03/unbutton-buttons.html" rel="related" type="text/html" /><published>2018-10-19T22:27:41Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This is a deep dive into how to un-style a <code>button</code>. In other words, how to make a button look like a link. It isn’t something you’d want to do all the time, but it does have its usefulness in select situations.</p><p>Thanks for putting this together Scott!</p>]]></content><amg:twitter><![CDATA[A deep dive into how to make a button element look like a link. Thanks for putting this together @scottohara!]]></amg:twitter><category term="forms" /><category term="accessibility" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/designing-a-textbox-unabridged/</id><title type="html"><![CDATA[🔗 Designing A Textbox, Unabridged]]></title><link href="https://www.aaron-gustafson.com/notebook/links/designing-a-textbox-unabridged/" rel="alternate" type="text/html" /><link href="https://www.smashingmagazine.com/2018/09/designing-a-textbox-unabridged/" rel="related" type="text/html" /><published>2018-10-03T22:07:51Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This is a super-detailed post about building a <code>textarea</code> with feedback about how much you’ve written in it, relative to the defined <code>maxlength</code>. There are some great recommendations here, especially:</p><ul><li>Start with sensible helper text;</li><li>Use JavaScript to smartly enhance the experience; and</li><li>Always provide a non-JavaScript fallback.</li></ul><p>I gave similar advice in <a href="https://webstandardssherpa.com/reviews/improving-the-tweet-box.html">a piece I wrote for <cite>Web Standards Sherpa</cite> a few years back</a>.</p>]]></content><amg:twitter><![CDATA[Here’s a thorough examination of how to progressively enhance a text area with support for (and exposure of) a maximum length]]></amg:twitter><category term="forms" /><category term="progressive enhancement" /></entry></feed>