{"version":"https://jsonfeed.org/version/1","title":"Aaron Gustafson: Content tagged user experience","description":"The latest 20 posts and links tagged user experience.","home_page_url":"https://www.aaron-gustafson.com","feed_url":"https://www.aaron-gustafson.com/feeds/user-experience.json","author":{"name":"Aaron Gustafson","url":"https://www.aaron-gustafson.com"},"icon":"https://www.aaron-gustafson.com/i/og-logo.png","favicon":"https://www.aaron-gustafson.com/favicon.png","expired":false,"items":[{"id":"https://www.aaron-gustafson.com/notebook/can-your-ai-pass-the-accessibility-test/","title":"✍🏻 Can Your AI Pass the Accessibility Test?","excerpt":"<p>Last week at Microsoft Build, <a href=\"https://www.linkedin.com/in/jessie-lorenz/\">Jessie Lorenz</a>, <a href=\"https://www.linkedin.com/in/cariefisher/\">Carie Fisher</a>, and I gave a short talk on a question every AI-enabled product team should be asking: can your AI pass the accessibility test?</p><p>The core point was straightforward: AI does not fix a broken process; it accelerates whatever process you already have. If accessibility is already in the workflow, AI can help scale inclusion. If it’s not, AI will scale the same barriers teams are already shipping.</p>","summary":"<p>Last week at Microsoft Build, <a href=\"https://www.linkedin.com/in/jessie-lorenz/\">Jessie Lorenz</a>, <a href=\"https://www.linkedin.com/in/cariefisher/\">Carie Fisher</a>, and I gave a short talk on a question every AI-enabled product team should be asking: can your AI pass the accessibility test?</p><p>The core point was straightforward: AI does not fix a broken process; it accelerates whatever process you already have. If accessibility is already in the workflow, AI can help scale inclusion. If it’s not, AI will scale the same barriers teams are already shipping.</p>","content_html":"<p>Last week at Microsoft Build, <a href=\"https://www.linkedin.com/in/jessie-lorenz/\">Jessie Lorenz</a>, <a href=\"https://www.linkedin.com/in/cariefisher/\">Carie Fisher</a>, and I gave a short talk on a question every AI-enabled product team should be asking: can your AI pass the accessibility test?</p>\n<p>The core point was straightforward: AI does not fix a broken process; it accelerates whatever process you already have. If accessibility is already in the workflow, AI can help scale inclusion. If it’s not, AI will scale the same barriers teams are already shipping.</p>\n<h2 id=\"full-transcript\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#full-transcript\" aria-hidden=\"true\">#</a> Full Transcript</h2>\n<figure id=\"figure-2026-06-10-01\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide3.png\" alt=\"Slide showing a typical software development lifecycle: project planning, design, development and coding, code review and CI/CD, public release, and customer feedback.\"></p>\n</figure>\n<p><strong>Jessie Lorenz:</strong> Everybody here ships software on a pipeline that looks something like this, right? Planning, design, development and coding, code review and CI/CD, public release, and then feedback. And then you kind of turn around and start the whole merry-go-round again, incorporating feedback back into planning.</p>\n<p>It is a six-stage pipeline. You know it. I know it too. I know it as a blind PM at Microsoft and I know it as someone who was born blind and often encounters the accessibility errors or barriers that get shipped.</p>\n<p>So hold on to this pipeline, because everything we are about to talk about lands somewhere on it.</p>\n<figure id=\"figure-2026-06-10-02\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide4.png\" alt=\"Slide titled The cost of inaccessibility, showing accessibility issues becoming 10x, 100x, and 1000x more expensive later in the lifecycle.\"></p>\n</figure>\n<p><strong>Jessie:</strong> Let us look at what happens when you solve your accessibility issues in the planning process. If you do that, it is a conversation. It is usually free to fix.</p>\n<p>Wait until design? Well, it is going to cost you ten times more. Wait until it reaches development? One hundred times more. If you do not notice your accessibility barriers until they actually ship, it compounds even more: one thousand times more.</p>\n<p>And each order of magnitude that compounds, if you really think about it, is a person, and multiples of people, being locked out of what you ship.</p>\n<p>Accessibility debt is a lot like security debt, and you would not just let security debt lie. Accessibility debt should be treated the same.</p>\n<figure id=\"figure-2026-06-10-03\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide5.png\" alt=\"Slide reading: AI accelerates the pace of software development… and the creation of accessibility issues.\"></p>\n</figure>\n<p><strong>Jessie:</strong> So we’re all excited about AI, right? Well, it accelerates the pace of software development and it also accelerates the creation of accessibility barriers. Both sides of the coin are true.</p>\n<p>Why?  AI is trained largely on a web that is inaccessible. AI learned from a web that is primarily inaccessible, full of barriers, and AI ships these barriers faster and in places where we’ve not seen them before.</p>\n<figure id=\"figure-2026-06-10-04\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide6.png\" alt=\"Slide titled Accessibility belongs in the workflow, with accessibility checks shown across the entire software development lifecycle.\"></p>\n</figure>\n<p><strong>Jessie:</strong> Accessibility belongs in your workflow. We are talking today not about slowing things down, not about trying to make things harder. We are actually talking about catching things early, catching things when they are cheaper, and implementing accessibility checks in every stage of the workflow.</p>\n<p>That could be a lint rule in the editor. That could be a gate in CI. It could be a flag in code review. It is the same workflow that we talked about before, the same pipeline. It is just that each step has an accessibility check on it.</p>\n<figure id=\"figure-2026-06-10-05\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide7.png\" alt=\"Slide titled Planning should be grounded in disability.\"></p>\n</figure>\n<p><strong>Jessie:</strong> I really hope I can make this real for you.</p>\n<p>People are always coming to talk to me about how to get money for their accessibility ideas. One guy came and talked to me and he was trying to get VC funding to re-carpet the San Francisco airport. He thought it would be a really good idea to give blind people canes that could guide them on this special carpet and the canes would have Bluetooth in them.</p>\n<p>Well, I saw this guy’s demo. He actually had some investor money lined up, and I asked him one simple question. It is the question I ask everyone: Did you talk to a single person with disabilities before creating this demo?</p>\n<p>The answer to my question is all too often no. Unfortunately, this is an example of what ungrounded planning looks like. You have a clever solution, but you are not solving the right problem.</p>\n<p>So the first step in creating an accessible software development lifecycle is to make sure accessibility is in your planning documents. That could be a question about how your feature will serve people with disabilities in a <a href=\"https://www.prfaq.org/\">PR/FAQ</a>. But if you do not put us in the roadmap, you are going to end up building something really beautiful that is answering the wrong question.</p>\n<figure id=\"figure-2026-06-10-06\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide8.png\" alt=\"Slide titled Building voice with disability at the center of voice, with statistics on abandoned Copilot voice tasks, disability prevalence, and neurodiverse employees performing better with Copilot.\"></p>\n</figure>\n<p><strong>Jessie:</strong> My team in Microsoft AI is building voice-first features in Copilot, and we have data that shows 37% of voice-initiated tasks in Copilot are abandoned. That is not good.</p>\n<p>We also have data that shows that more than one billion people in the world live with some kind of disability, a limitation to one or more major life activities. And 76% of folks who have dyslexia or other neurological impairments say that they are better at work when they can use Copilot.</p>\n<p>So what the data shows us is that when Copilot voice breaks, it hits the people who need it most first and hardest. That is why we created a feature called Speak to Done, and that is how it got on our roadmap.</p>\n<p>Now I am going to pass it to Aaron.</p>\n<figure id=\"figure-2026-06-10-07\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide9.png\" alt=\"Slide titled Design tools need to make inclusive design easy.\"></p>\n</figure>\n<p><strong>Aaron Gustafson:</strong> Once the planning is done, most teams begin designing the user experience and the user interface. Now, whether they are using traditional design software or newer vibe-design approaches, it is imperative that designers have the right tools and information to help them make accessible choices.</p>\n<figure id=\"figure-2026-06-10-08\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide10.png\" alt=\"Slide for Accessibility Assistant for Figma, showing the plugin UI and an annotated design.\"></p>\n</figure>\n<p><strong>Aaron:</strong> One of the efforts that I have had the pleasure of working on in this space is the <a href=\"https://aka.ms/AccessibilityAssistantForFigma\">Accessibility Assistant plugin for Figma</a>. It offers a suite of tools to help designers clarify the intent of their interface and assess the quality of their design when it comes to supporting people with disabilities.</p>\n<p>Here on the screen I have a visual showing an annotated UI. Those annotations can actually act as an accessibility spec to guide engineering work. In fact, we are working on a new feature that will allow designers to export this accessibility information and hand that off to developers, whether they are human or whether they are agentic.</p>\n<p>We have seen some really positive results from our early tests of handing off that accessibility spec to agentic workflows, both in being able to fix existing bugs in the interface based on the accessibility spec and in being able to build interfaces from scratch. They are even able to take simple things like an example row in a grid and extrapolate the accessibility annotations for that one row out to all the rows within the grid, which is really exciting.</p>\n<p>The demo also shows one of the visualization tools that we have. We have a bunch of tools we are adding in for designers to help them understand how their designs end up being understood or experienced by people. In this case, we have done a focus order overlay to visually display how somebody would move through the interface.</p>\n<p>By having this information early, and as Jessie said, in a way that is very cheap to make changes, improving accessibility at the design stage can take mere moments, but it can have a huge impact. It costs a lot more to change that once it is already ensconced in code.</p>\n<figure id=\"figure-2026-06-10-09\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide11.png\" alt=\"Slide titled All code must be consistent and validated.\"></p>\n</figure>\n<p><strong>Aaron:</strong> Even with a rigorous design system and robust accessibility specs, it’s still critical to embed accessibility into the coding process. As Jessie mentioned at the beginning of this talk, that becomes even more critical in the era of AI-assisted coding.</p>\n<figure id=\"figure-2026-06-10-10\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide12.png\" alt=\"Slide titled Coding models aren’t great on their own, showing accessibility pass rates improving from base models to instruction files, skills, and deterministic tools.\"></p>\n</figure>\n<p><strong>Aaron:</strong> Out of the box, most coding agents are pretty terrible when it comes to accessibility. That’s not surprising though. As Jessie said, they’re trained on what we created, and the web we created has not been all that accessible, so they learned from us.</p>\n<p>Left to their own devices, most of the code they write only passes about 8–25% of automatable accessibility checks.</p>\n<p>Instruction files are often touted for their ability to steer models toward better outcomes by teaching them what the accessibility expectations should be up front, but even with that the pass rate only climbs to 37–60% of automatable checks.</p>\n<p>When you start imbuing agents with skills, you get closer to a pass rate of roughly 86%, but it is not until you give them actual deterministic tests to run, and instructions on how to use them, that you get them to iterate until the code passes all of those automatable checks.</p>\n<p>But even that only gets you so far because it takes you to the limit of what is testable purely in an automated fashion, via unit tests and integration tests. It does not cover things like usability, so I want to put it in context: automatable tests only cover about 50% of what you need for a UI to be considered truly accessible.</p>\n<p>Still, every step in the right direction counts. And now that we have discussed accessibility in the code authoring context, I am going to hand it off to Carie to talk about embedding accessibility in the code review process.</p>\n<figure id=\"figure-2026-06-10-11\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide13.png\" alt=\"Slide titled Code review is the last line of defense.\"></p>\n</figure>\n<p><strong>Carie Fisher:</strong> I am from GitHub, and we made an <a href=\"https://github.com/github/accessibility-scanner\">Accessibility Scanner</a>. It’s open source. If you have not checked out the booth, we have a demo there.</p>\n<p>What we are talking about here is bringing accessibility alongside the other feedback developers already expect. In pull requests, it should show up alongside code quality and security feedback. In CI/CD, accessibility checks should run where automation makes sense, whether you are using GitHub Actions, Azure Pipelines, browser tests, or deterministic tests. We want to be where you are and make sure accessibility is considered.</p>\n<p>After release, issues and feedback should flow back into the backlog and into better patterns for next time. The PR is necessary, but it is not enough. If accessibility only appears at the end, we are still fixing after the fact.</p>\n<p>The bigger direction here is to embed accessibility into AI tooling and engineering practices so innovation scales inclusion, not exclusion.</p>\n<figure id=\"figure-2026-06-10-12\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide14.png\" alt=\"Slide for GitHub’s AI-powered accessibility scanner, showing a Find, File, Fix workflow.\"></p>\n</figure>\n<p><strong>Carie:</strong> GitHub’s AI-powered Accessibility Scanner makes that workflow concrete with a simple loop: Find, File, Fix.</p>\n<p>First, it helps find repeatable accessibility issues. Then it files actionable GitHub issues instead of leaving teams with a separate report that lives outside their workflow. And then those issues become useful context for fixing, including with Copilot-assisted remediation.</p>\n<p>Under the hood, we’re using Deque’s aXe ruleset, which is basically the gold standard for automated accessibility checks. It is free, it is open source, and we created the GitHub workflow around it so you can scan your page for accessibility errors, create issues, and then even assign those issues to Copilot to draft a pull request.</p>\n<p>We deliberately left a spot for a human to stay in the loop and make sure the result is truly accessible. As Aaron and Jessie have said, automated checks may only catch about half of the issues overall. You still need manual checks and you still need to work with people with disabilities to make sure your product is really inclusive.</p>\n<figure id=\"figure-2026-06-10-13\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide15.png\" alt=\"Screenshot of GitHub’s accessibility scanner repository shown during the live demo.\"></p>\n</figure>\n<p><strong>Carie:</strong> We don’t have time to run through the full demo on stage, but this is the workflow we wanted people to remember. Accessibility is not happening in a separate process. It is part of the same system teams already use to build, triage, review, automate, and ship.</p>\n<figure id=\"figure-2026-06-10-14\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide16.png\" alt=\"Slide titled When accessibility is integrated, magic happens.\"></p>\n</figure>\n<p><strong>Carie:</strong> When accessibility is integrated, magic happens.</p>\n<figure id=\"figure-2026-06-10-15\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide17.png\" alt=\"Slide titled Voice to Done, showing a natural-language voice request turned into completed tasks in Copilot.\"></p>\n</figure>\n<p><strong>Jessie:</strong> I am a blind PM at Microsoft and most days I spend them telling software what I want it to do and then spending an inordinate amount of time cleaning up what it did wrong.</p>\n<p>My team owns two really powerful features called Copilot Tasks and Copilot Cowork. They are super powerful. However, they did not have voice included in them, so we created a feature called Speak to Done, which is the voice layer over Copilot Tasks and Copilot Cowork.</p>\n<p>I said, “Find that track meet email from three weeks ago, add it to my calendar, and text the other parent.”</p>\n<p>Copilot did not lose context, did not break, found the track meet in the Gmail thread, put it on the calendar, and sent it to the other parent without breaking.</p>\n<p>Now let me be clear: without the shift-left practices, without the tools that we talked about today, the Figma plugin at the design process, the deterministic testing, the code review, none of these features reach me.</p>\n<p>Speak to Done is really cool because it solves a problem that disproportionately impacts folks with disabilities, like we talked about earlier, but it also helps everybody who wants to use Copilot with their voice.</p>\n<p>In disability circles, we talk a lot about the curb-cut effect. Back in the 1960s, they did not have curb cuts. Wheelchair users in Berkeley literally started taking sledgehammers to the curbs and making them themselves. Now we see curb cuts everywhere, and who benefits? People who use strollers, delivery workers, all of society.</p>\n<p>That is a little bit of what we are trying to convey today. When we are talking about the things we build, we are not talking about tools. We are talking about who gets to use what we ship.</p>\n<p>I encourage you to be conscious about creating a more accessible world, one code snippet at a time.</p>\n<hr>\n<p>If you want to dig into the tools we referenced, here is <a href=\"https://aka.ms/a11y-scanner-resources\">the resource page for the talk</a>.</p>\n","social_text":"Last week at Microsoft Build, Jessie Lorenz, Carie Fisher, and I talked about how to build accessibility into AI-assisted software development from planning through code review and release.","url":"https://www.aaron-gustafson.com/notebook/can-your-ai-pass-the-accessibility-test/","tags":["accessibility","AI/ML","inclusive design","Microsoft","user experience"],"image":"https://www.aaron-gustafson.com/i/posts/2026-06-10/Slide2.png","date_published":"2026-06-10T18:32:00Z"},{"id":"https://www.aaron-gustafson.com/speaking-engagements/can-your-ai-pass-the-accessibility-test/","title":"📢 Can Your AI Pass the Accessibility Test?","summary":"<p>At Microsoft Build 2026, Jessie Lorenz, Carie Fisher, and I explored how to embed accessibility across the software development lifecycle so AI scales inclusion rather than exclusion.</p>","content_html":"<p>At Microsoft Build 2026, Jessie Lorenz, Carie Fisher, and I looked at what happens when AI accelerates software development without accessibility built into the workflow. We walked through the full software development lifecycle, from planning and design through coding, code review, CI/CD, and feedback loops, and showed how accessibility checks, tooling, and human review can help teams catch problems earlier.</p>\n<p>Along the way, we shared examples from Microsoft and GitHub, including the Accessibility Assistant for Figma, deterministic testing for AI-assisted coding workflows, and GitHub’s Accessibility Scanner. The through line was simple: if accessibility is part of the process, AI can help scale inclusion; if it is missing, AI can scale exclusion just as quickly.</p>\n","social_text":"At Microsoft Build 2026, Jessie Lorenz, Carie Fisher, and I explored how to embed accessibility across the software development lifecycle so AI scales inclusion rather than exclusion.","url":"https://www.aaron-gustafson.com/speaking-engagements/can-your-ai-pass-the-accessibility-test/","tags":["accessibility","AI/ML","inclusive design","Microsoft","user experience"],"image":"https://www.aaron-gustafson.com/undefined","date_published":"2026-06-02T00:09:00Z"},{"id":"https://www.aaron-gustafson.com/notebook/visual-validation-feedback-for-form-fields/","title":"✍🏻 Visual Validation Feedback for Form Fields","excerpt":"<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":"<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>","content_html":"<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>\n<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>\n<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>\n<hr>\n<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>\n<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\">></span></span>\n  <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\">></span></span>Username:<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>label</span><span class=\"token punctuation\">></span></span>\n  <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\">/></span></span>\n\n  <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\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n      <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\">></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\">></span></span>\n      <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\">></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\">></span></span>\n      <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\">></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\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span>\n\n  <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\">></span></span>Submit<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<p>The component:</p>\n<ol>\n<li>Associates with an input via the <code>for</code> attribute (just like a <code>label</code> element)</li>\n<li>Finds all elements with <code>data-pattern</code> attributes</li>\n<li>Tests the input value against each pattern when the configured trigger fires</li>\n<li>Adds <code>validation-matched</code> or <code>validation-unmatched</code> classes and visual indicators accordingly</li>\n<li>Inserts localized, visually hidden state text once the field has a value</li>\n<li>Updates a single polite live region while users type</li>\n<li>Uses <code>setCustomValidity()</code> to integrate with native form validation</li>\n<li>Prevents form submission until all rules match</li>\n</ol>\n<p>The cascade animation, controlled by <code>each-delay</code>, adds a subtle sequential effect as rules are checked. Small detail, meaningful feedback.</p>\n<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>\n<p>Define rules using regular expression patterns in the <code>data-pattern</code> attribute:</p>\n<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\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token comment\">&lt;!-- Length requirements --></span>\n    <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\">></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\">></span></span>\n    <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\">></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\">></span></span>\n\n    <span class=\"token comment\">&lt;!-- Character type requirements --></span>\n    <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\">></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\">></span></span>\n    <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\">></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\">></span></span>\n    <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\">></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\">></span></span>\n    <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;*]+<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></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\">></span></span>\n\n    <span class=\"token comment\">&lt;!-- Format requirements --></span>\n    <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\">></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\">></span></span>\n    <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-zA-Z0-9]+$<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></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\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<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>\n<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\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n    <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\">></span></span>Contains @ symbol<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>li</span><span class=\"token punctuation\">></span></span>\n    <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\">></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\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<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>\n<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\">></span></span>\n  <span class=\"token comment\">&lt;!-- rules --></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span></code></pre>\n<p>Set it to “0” to remove the cascade effect entirely and check all rules simultaneously.</p>\n<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>\n<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>\n<p>Here’s a complete example:</p>\n<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\">></span></span><span class=\"token style\"><span class=\"token language-css\">\n  <span class=\"token selector\">.is-valid</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token property\">border-color</span><span class=\"token punctuation\">:</span> green<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token selector\">.is-invalid</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token property\">border-color</span><span class=\"token punctuation\">:</span> red<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token selector\">.rule-pass</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token property\">color</span><span class=\"token punctuation\">:</span> green<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token selector\">.rule-fail</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token property\">color</span><span class=\"token punctuation\">:</span> red<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n</span></span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>style</span><span class=\"token punctuation\">></span></span>\n\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>form-validation-list</span>\n  <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>\n  <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>\n  <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>\n  <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>\n  <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>\n<span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n    <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\">></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\">></span></span>\n    <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\">></span></span>Special char (!@#)<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>li</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<ul>\n<li><code>--rule-matched-icon</code> - Content for matched state (default: “✓”)</li>\n<li><code>--rule-unmatched-icon</code> - Content for unmatched state (default: “✗”)</li>\n<li><code>--rule-icon-size</code> - Size of icons (default: 1em)</li>\n<li><code>--rule-matched-color</code> - Color for matched rules (default: green)</li>\n<li><code>--rule-unmatched-color</code> - Color for unmatched rules (default: red)</li>\n</ul>\n<p>The older <code>--validation-*</code> custom property names are still supported as legacy aliases.</p>\n<p>Here’s an example of that:</p>\n<pre class=\"language-css\" tabindex=\"0\"><code class=\"language-css\"><span class=\"token selector\">form-validation-list</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">--rule-matched-icon</span><span class=\"token punctuation\">:</span> <span class=\"token string\">\"✅\"</span><span class=\"token punctuation\">;</span>\n  <span class=\"token property\">--rule-unmatched-icon</span><span class=\"token punctuation\">:</span> <span class=\"token string\">\"❌\"</span><span class=\"token punctuation\">;</span>\n  <span class=\"token property\">--rule-icon-size</span><span class=\"token punctuation\">:</span> 1.2em<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">--rule-matched-color</span><span class=\"token punctuation\">:</span> #28a745<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">--rule-unmatched-color</span><span class=\"token punctuation\">:</span> #dc3545<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<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>\n<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>\n<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>\n<p>If you really want to get into the weeds, you can also listen for validation changes in your JavaScript code:</p>\n<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>\n\nvalidationList<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\">=></span> <span class=\"token punctuation\">{</span>\n  <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>\n  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\">`</span><span class=\"token string\">Matched </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>matchedRules<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\"> of </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>totalRules<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\"> rules</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  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\">`</span><span class=\"token string\">Field is </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>isValid <span class=\"token operator\">?</span> <span class=\"token string\">\"valid\"</span> <span class=\"token operator\">:</span> <span class=\"token string\">\"invalid\"</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<p>The event fires after validation completes and gives you the current state. Nice and tidy.</p>\n<p>You can also manually trigger validation and check the element’s current state at any time:</p>\n<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>\n\n<span class=\"token comment\">// Trigger validation</span>\n<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>\nconsole<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>\n\n<span class=\"token comment\">// Check current state</span>\nconsole<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>\n<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>\n<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>\n<pre class=\"language-html\" tabindex=\"0\"><code class=\"language-html\"><span class=\"token comment\">&lt;!-- Spanish --></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>form-validation-list</span>\n  <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>\n  <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>\n  <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>\n  <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>\n  <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>\n<span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n    <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\">></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\">></span></span>\n    <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\">></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\">></span></span>\n    <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\">></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\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span>\n\n<span class=\"token comment\">&lt;!-- French --></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>form-validation-list</span>\n  <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>\n  <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>\n  <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>\n  <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>\n  <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>\n<span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n    <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\">></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\">></span></span>\n    <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\">></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\">></span></span>\n    <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\">></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\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<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>\n<p>The component is built with accessibility in mind:</p>\n<ul>\n<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>\n<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>\n<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>\n<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>\n</ul>\n<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>\n<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>\n<p>The component uses <code>setCustomValidity()</code> to participate in native form validation:</p>\n<ul>\n<li>When all rules match, custom validity is cleared</li>\n<li>When rules don’t match, a custom validity message is set</li>\n<li>Form submission is prevented until all rules pass</li>\n<li>Works with <code>:valid</code> and <code>:invalid</code> CSS pseudo-classes</li>\n<li>Compatible with the Constraint Validation API</li>\n</ul>\n<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>\n<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>\n\nform<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\">=></span> <span class=\"token punctuation\">{</span>\n  <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>\n    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>\n    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>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<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>\n<p>Here’s a complete password validation setup:</p>\n<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\">></span></span>\n  <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\">></span></span>Password:<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>label</span><span class=\"token punctuation\">></span></span>\n  <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\">/></span></span>\n\n  <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\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n      <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\">></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\">></span></span>\n      <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\">></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\">></span></span>\n      <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\">></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\">></span></span>\n      <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\">></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\">></span></span>\n      <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;*]+<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>\n        At least one special character (!@#$%^&amp;*)\n      <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>li</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-validation-list</span><span class=\"token punctuation\">></span></span>\n\n  <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\">></span></span>Submit<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<h2 id=\"play-with-it\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#play-with-it\" aria-hidden=\"true\">#</a> Play with it</h2>\n<p>Check out <a href=\"https://aarongustafson.github.io/form-validation-list/demo/\">the demo</a> with various examples:</p>\n<figure id=\"fig-2025-12-06-09\" class=\"media-container\">\n<fullscreen-control class=\"talk__slides__embed video-embed__video\">\n<iframe src=\"https://aarongustafson.github.io/form-validation-list/demo/\" class=\"talk__slides__embed video-embed__video\" frameborder=\"0\"></iframe>\n</fullscreen-control>\n</figure>\n<h2 id=\"grab-it\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#grab-it\" aria-hidden=\"true\">#</a> Grab it</h2>\n<p>View the project on <a href=\"https://github.com/aarongustafson/form-validation-list\">GitHub</a>.</p>\n<p>Install via <code>npm</code>:</p>\n<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>\n<p>For most projects, import the guarded auto-definition helper:</p>\n<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>\n<p>If you want to control the tag name yourself, import <code>FormValidationListElement</code> and register it manually.</p>\n<p>If you give it a spin, I’d love to hear how it works in your forms. Happy validating!</p>\n","social_text":"New #WebComponent: Show users which validation requirements they’ve met—as they type.","url":"https://www.aaron-gustafson.com/notebook/visual-validation-feedback-for-form-fields/","tags":["web components","progressive enhancement","forms","HTML","JavaScript","web forms","user experience"],"date_published":"2026-04-22T20:17:47Z"},{"id":"https://www.aaron-gustafson.com/notebook/never-lose-form-progress-again/","title":"✍🏻 Never Lose Form Progress Again","excerpt":"<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":"<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>","content_html":"<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>\n<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>\n<h2 id=\"basic-usage\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#basic-usage\" aria-hidden=\"true\">#</a> Basic usage</h2>\n<p>All you need to do is wrap your form in the component:</p>\n<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\">></span></span>\n  <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\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>label</span><span class=\"token punctuation\">></span></span>\n      Name\n      <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\">/></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>label</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>label</span><span class=\"token punctuation\">></span></span>\n      Email\n      <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\">/></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>label</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>label</span><span class=\"token punctuation\">></span></span>\n      Message\n      <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\">></span></span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>textarea</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>label</span><span class=\"token punctuation\">></span></span>\n    <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\">></span></span>Send<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-saver</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<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>\n<p><code>form-saver</code> supports the controls most of us reach for every day:</p>\n<ul>\n<li>Text-style <code>input</code> fields</li>\n<li><code>textarea</code> elements,</li>\n<li><code>select</code> elements (including multi-selects), and</li>\n<li><code>checkbox</code> and <code>radio</code> controls.</li>\n</ul>\n<p>File inputs are intentionally excluded.</p>\n<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>\n<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>\n<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>\n<p>That is what the <code>retain</code> attribute is for:</p>\n<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\">></span></span>\n  <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\">></span></span>\n    …\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-saver</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<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>\n<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>\n<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>\n  <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>\n  <span class=\"token attr-name\">retain-choice</span>\n  <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>\n<span class=\"token punctuation\">></span></span>\n  <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\">></span></span>\n    …\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-saver</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<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>\n  <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>\n  <span class=\"token attr-name\">retain-choice</span>\n  <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>\n  <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>\n<span class=\"token punctuation\">></span></span>\n  <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\">></span></span>\n    …\n    <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\">></span></span>\n      <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\">></span></span>Send<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-saver</span><span class=\"token punctuation\">></span></span></code></pre>\n<p>That gives you a lot more control over the experience without making you build the retention UI yourself.</p>\n<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>\n<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>\n<p>If you need something more explicit, you can provide your own <code>storage-key</code>:</p>\n<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\">></span></span>\n  <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\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>label</span><span class=\"token punctuation\">></span></span>\n      Street Address\n      <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\">/></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>label</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>label</span><span class=\"token punctuation\">></span></span>\n      Postal Code\n      <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\">/></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>label</span><span class=\"token punctuation\">></span></span>\n    <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\">></span></span>Continue<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>form-saver</span><span class=\"token punctuation\">></span></span></code></pre>\n<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>\n<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>\n<p>If you need more direct control, the component exposes a few methods:</p>\n<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>\n\n<span class=\"token comment\">// Persist the current state</span>\nsaver<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>\n\n<span class=\"token comment\">// Restore previously saved values</span>\nsaver<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>\n\n<span class=\"token comment\">// Clear out anything stored for this form</span>\nsaver<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>\n<p>That can be useful when you want to pair it with your own UI, analytics, or custom workflow around save and restore.</p>\n<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>\n<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>\n<p>That’s a pretty good trade-off.</p>\n<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>\n<h2 id=\"demo\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#demo\" aria-hidden=\"true\">#</a> Demo</h2>\n<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>\n<figure id=\"fig-2026-04-20-01\" class=\"media-container\">\n<fullscreen-control class=\"talk__slides__embed video-embed__video\">\n<iframe src=\"https://aarongustafson.github.io/form-saver/demo/\" class=\"talk__slides__embed video-embed__video\" frameborder=\"0\"></iframe>\n</fullscreen-control>\n</figure>\n<h2 id=\"grab-it\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#grab-it\" aria-hidden=\"true\">#</a> Grab it</h2>\n<p>The project is available on <a href=\"https://github.com/aarongustafson/form-saver\">GitHub</a>, and you can install it from npm:</p>\n<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>\n<p>If you want the easiest path, just import it and let the component register itself:</p>\n<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>\n<p>If you would rather define it yourself, you can import the class directly:</p>\n<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>\n\ncustomElements<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>\n<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>\n","social_text":"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.","url":"https://www.aaron-gustafson.com/notebook/never-lose-form-progress-again/","tags":["web components","forms","HTML","JavaScript","progressive enhancement","web forms","user experience"],"date_published":"2026-04-20T23:59:08Z"},{"id":"https://www.aaron-gustafson.com/notebook/accessibility-assistant-for-figma-v52/","title":"✍🏻 Accessibility Assistant for Figma v52","excerpt":"<p>I just hit “publish” on <a href=\"https://www.figma.com/community/plugin/731310036968334777/accessibility-assistant\">Accessibility Assistant for Figma</a> v52, and I wanted to share why this release matters so much to our team.</p>","summary":"<p>I just hit “publish” on <a href=\"https://www.figma.com/community/plugin/731310036968334777/accessibility-assistant\">Accessibility Assistant for Figma</a> v52, and I wanted to share why this release matters so much to our team.</p>","content_html":"<p>I just hit “publish” on <a href=\"https://www.figma.com/community/plugin/731310036968334777/accessibility-assistant\">Accessibility Assistant for Figma</a> v52, and I wanted to share why this release matters so much to our team.</p>\n<p>We’re in the middle of a major overhaul. To get there, we needed a lot of foundational infrastructure work to modernize the plugin and prepare for new features that help designers produce more accessible work, faster. This release includes a big chunk of that groundwork, notably:</p>\n<ul>\n<li>Annotations are now presented as Figma-native Dev Mode annotations; this greatly reduces the working overhead of the plugin and reduces visual clutter in the document. We’ve also color-coordinated the icons in the Annotation Set viewer to the labels you see in the Dev Mode annotations, making it easier to scan.</li>\n<li>Legacy annotation tables will automatically be migrated into the new system. The visual readout tables will be hidden when this happens, but are still accessible if you need to copy or reference them. We’ve also included a tool to clean up these old layers when you’re ready.</li>\n<li>Annotations are now managed in a single UI rather than split by focus-order impact. That means you no longer need to jump back and forth between tools to annotate designs correctly.</li>\n<li>We’ve organized and expanded the list of W3C roles available in the role picker. Additionally, the form now adapts to the role, offering you only the relevant fields and reducing distraction. We also added a description field, should you need it.</li>\n</ul>\n<p>We also fixed duplication bugs. You can now copy layers and keep their annotations as a new Annotation Set. Similarly, duplicating pages now regenerates page-bound annotations automatically. On especially large pages, that regeneration may take a little time.</p>\n<p>This release has been a long time coming, and I’m incredibly proud of the team behind it, especially Ashish Singh from HCL, and Michael Fairchild, Scott O’Hara, and Ben Truelove from Microsoft. Their care, and their depth of accessibility knowledge, has been instrumental in getting this project to where it is today.</p>\n<p>And there’s more to come!</p>\n","social_text":"I just hit “publish” on Accessibility Assistant for Figma v52 and I wanted to share some details on why this is a monumental release for us.","url":"https://www.aaron-gustafson.com/notebook/accessibility-assistant-for-figma-v52/","tags":["accessibility","design","inclusive design","Microsoft","user experience","WAI-ARIA"],"date_published":"2026-02-20T23:27:28Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/designing-for-distress-understanding-users-in-crisis/","title":"🔗 Designing for Distress: Understanding Users in Crisis","content_html":"<blockquote>\n<p>In a distressing moment, it’s like you’re rushing to the airport — you’re just looking for help right now. When you aren’t distressed, it’s like you’re on vacation. You can take your time, you’re more open to exploring.</p>\n</blockquote>\n<p>In a recent study, the VA learned a lot from users navigating acute distress — and why typical UX patterns fail. This is highly recommended reading for anyone working in the design space.</p>\n","social_text":"In a recent study, the VA learned a lot from users navigating acute distress — and why typical UX patterns fail.","url":"https://www.aaron-gustafson.com/notebook/links/designing-for-distress-understanding-users-in-crisis/","external_url":"https://medium.com/@codybolandphd/designing-for-distress-understanding-users-in-crisis-0e02466f1f5b","tags":["accessibility","user experience"],"date_published":"2025-10-10T18:52:07Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/disability-personas-from-a-web-for-everyone/","title":"🔗 Disability Personas from A Web for Everyone","summary":"<p>The disability personas contained in Sarah Horton &amp; Whitney Quesenbery’s A Web for Everyone are a terrific resource, so I’m thrilled their available beyond the book now too.</p>","content_html":"<p>The disability personas contained in Sarah Horton &amp; Whitney Quesenbery’s <cite>A Web for Everyone</cite> are a terrific resource, so I’m thrilled their available beyond the book now too.</p>\n","social_text":"I ❤️ these disability personas. What an amazing resource!","url":"https://www.aaron-gustafson.com/notebook/links/disability-personas-from-a-web-for-everyone/","external_url":"https://knowaboutaccessibility.org/category/resources/","tags":["accessibility","inclusive design","user experience"],"date_published":"2025-05-09T23:05:56Z"},{"id":"https://www.aaron-gustafson.com/speaking-engagements/expanding-accessibility-with-ai/","title":"📢 Expanding Accessibility with AI","summary":"<p>This session, hosted by Aaron Gustafson and Ioana Tanase explores the transformative role of AI in enhancing accessibility. Together, you will uncover the pivotal role AI plays in crafting cutting-edge accessible technologies, learn about innovative AI tools that empower individuals with disabilities, and dive into best practices for creating inclusive AI solutions. Together, we will also explore the exciting future of AI in pushing the boundaries of accessibility.</p>","content_html":"<p>This session, hosted by Aaron Gustafson and Ioana Tanase explores the transformative role of AI in enhancing accessibility. Together, you will uncover the pivotal role AI plays in crafting cutting-edge accessible technologies, learn about innovative AI tools that empower individuals with disabilities, and dive into best practices for creating inclusive AI solutions. Together, we will also explore the exciting future of AI in pushing the boundaries of accessibility.</p>\n<p>Learning objectives:</p>\n<ol>\n<li>Understand the pivotal role AI plays in crafting cutting-edge accessible technologies.</li>\n<li>Learn about innovative AI tools that empower individuals with disabilities.</li>\n<li>Dive into best practices for creating inclusive AI solutions.</li>\n<li>Explore the exciting future of AI in pushing the boundaries of accessibility</li>\n</ol>\n","social_text":"This session, hosted by Aaron Gustafson and Ioana Tanase explores the transformative role of AI in enhancing accessibility. Together, you will uncover the pivotal role AI plays in crafting cutting-edge accessible technologies, learn about innovative AI tools that empower individuals with disabilities, and dive into best practices for creating inclusive AI solutions. Together, we will also explore the exciting future of AI in pushing the boundaries of accessibility.","url":"https://www.aaron-gustafson.com/speaking-engagements/expanding-accessibility-with-ai/","tags":["accessibility","AI/ML","inclusive design","the future","user experience","voice UX"],"image":"https://www.aaron-gustafson.com/undefined","date_published":"2024-12-11T08:08:30Z"},{"id":"https://www.aaron-gustafson.com/speaking-engagements/embedding-accessibility/","title":"📢 Embedding Accessibility","summary":"<p>In this session, I discuss the many reasons companies should prioritize accessibility, how to build a culture of accessibility, and what it can mean for their bottom line.</p>","content_html":"<p>In this session, I discuss the many reasons companies should prioritize accessibility, how to build a culture of accessibility, and what it can mean for their bottom line.</p>\n","social_text":"In this session, I discuss the many reasons companies should prioritize accessibility, how to build a culture of accessibility, and what it can mean for their bottom line.","url":"https://www.aaron-gustafson.com/speaking-engagements/embedding-accessibility/","tags":["accessibility","inclusion","inclusive design","progressive enhancement","user experience"],"image":"https://www.aaron-gustafson.com/undefined","date_published":"2024-01-19T08:08:00Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/the-ux-of-html/","title":"🔗 The UX of HTML","content_html":"<blockquote>\n<p>Somehow my students are allergic to semantics and shit. And they’re not alone. If you look at 99% of all websites in the wild, everybody who worked on them seems to be allergic to semantics and shit. On most websites heading levels are just random numbers, loosely based on font-size. Form fields have no labels. Links and buttons are divs. It’s really pretty bad. So it’s not just my students, the whole industry doesn’t understand semantics and shit.</p>\n</blockquote>\n<p>I feel this… deeply. And I 100% agree with where Vasilis is coming from here. I do take a bit of umbrage with the idea that heading levels don’t matter—they really do—but his point about getting folks excited about the stuff they get for free by paying attention to their markup is something I’ve been pushing for years as well.</p>\n<p>If you’re interested in a related deep dive into HTML’s lack of dependencies, check out <a href=\"https://www.smashingmagazine.com/2016/05/developing-dependency-awareness/\">this piece I wrote for <cite>Smashing Magazine</cite></a>. If you’d like to dive deeper into forms, I have <a href=\"/speaking-engagements/the-features-of-highly-effective-forms/\">this talk you might like</a>.</p>\n","social_text":"We should absolutely be (and continue) touting the #UX freebies we get with #HTML.","url":"https://www.aaron-gustafson.com/notebook/links/the-ux-of-html/","external_url":"https://www.htmhell.dev/adventcalendar/2023/1/","tags":["progressive enhancement","user experience","HTML"],"image":"https://www.htmhell.dev/images/og/advent23_1.png?s=011221s","date_published":"2024-01-05T22:52:50Z"},{"id":"https://www.aaron-gustafson.com/notebook/sharing-in-the-age-of-3p-cookie-mageddon/","title":"✍🏻 Sharing in the Age of 3p Cookie-mageddon","excerpt":"<p>Over a decade ago, I wrote up <a href=\"https://blog.easy-designs.net/archives/dont-sell-out-your-users/\">detailed instructions on how to enable users to share your content on social media without allowing them to be tracked by every social media site via cookies</a>. In a few short weeks <a href=\"https://developers.google.com/privacy-sandbox/blog/cookie-countdown-2023oct\">“third party” cookies will get the boot in Chromium-based browsers</a>. If you’re still relying on third party share widgets on your site, your users may start seeing problems. Now is a good time to replace them with code that Just Works™. Here’s how…</p>","summary":"<p>Over a decade ago, I wrote up <a href=\"https://blog.easy-designs.net/archives/dont-sell-out-your-users/\">detailed instructions on how to enable users to share your content on social media without allowing them to be tracked by every social media site via cookies</a>. In a few short weeks <a href=\"https://developers.google.com/privacy-sandbox/blog/cookie-countdown-2023oct\">“third party” cookies will get the boot in Chromium-based browsers</a>. If you’re still relying on third party share widgets on your site, your users may start seeing problems. Now is a good time to replace them with code that Just Works™. Here’s how…</p>","content_html":"<p>Over a decade ago, I wrote up <a href=\"https://blog.easy-designs.net/archives/dont-sell-out-your-users/\">detailed instructions on how to enable users to share your content on social media without allowing them to be tracked by every social media site via cookies</a>. In a few short weeks <a href=\"https://developers.google.com/privacy-sandbox/blog/cookie-countdown-2023oct\">“third party” cookies will get the boot in Chromium-based browsers</a>. If you’re still relying on third party share widgets on your site, your users may start seeing problems. Now is a good time to replace them with code that Just Works™. Here’s how…</p>\n<h2 id=\"sharing%2C-the-old-fashioned-way\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#sharing%2C-the-old-fashioned-way\" aria-hidden=\"true\">#</a> Sharing, the Old-fashioned Way</h2>\n<p>When it comes to sharing, there are myriad ways to do it. If you’re at all familiar with my work, it should come as no surprise that I always start with a universally-useable and accessible baseline and then <a href=\"/tags/progressive-enhancement\">progressively enhance</a> things from there. Thankfully, every social media site I commonly use (with the exception of the Fediverse) makes this pretty easy by providing a form that accepts inbound content via the query string.<sup class=\"footnote-ref\"><a href=\"#fn1\" id=\"fnref1\">1</a></sup> You can <a href=\"https://www.linkedin.com/cws/share?url=https://www.aaron-gustafson.com/notebook/rebuilding-a-php-app-using-isomorphic-javascript-with-eleventy-and-netlify/\">try LinkedIn’s to see it in action</a>.</p>\n<p>Each service is a little different, but all function similarly. I support the following ones in this site:</p>\n<table>\n<caption>Social Media Sites and Their Sharing URLs</caption>\n<thead>\n<tr>\n<th scope=\"col\">Site</th>\n<th scope=\"col\">Destination</th>\n<th scope=\"col\">URL</th>\n<th scope=\"col\">Optional Params</th>\n</tr>\n</thead>\n<tbody>\n<tr><th scope=\"row\">Twitter / X</th><td>https://twitter.com/�4�intent/tweet�5��6��7�url�8��9��10��11��12�\n<tr><th scope=\"row\">Hacker News</th><td>https://news.ycombinator.com/�4�submitlink�5��6��7�u�8��9��10��11�t�12� = the title you want to share </td></tr>\n<tr><th scope=\"row\">Facebook</th><td>https://www.facebook.com/�4�sharer.php�5��6��7�u�8��9��10��11��12�\n<tr><th scope=\"row\">LinkedIn</th><td>https://www.linkedin.com/cws/share�4��5��6�url�7��8��9��10��11�\n<tr><th scope=\"row\">Pinterest</th><td>https://pinterest.com/�4�pin/create/button/�5��6��7�url�8��9��10��11�media�12� = an image to share<br><code>description</code> = the text you want to share</td></tr>\n</tbody>\n</table>\n<p>Using this information, I created <a href=\"https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/_includes/partials/post/sharing.njk\">a partial template for use on any page in this site</a> (though I mainly use it on blog posts right now). Each link includes useful text content (e.g., “Share on <strong>__</strong>”) and a local SVG of the service’s icon. Here’s a simplified overview of the markup I use:</p>\n<pre class=\"language-html\" tabindex=\"0\"><code class=\"language-html\"><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</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>social-links social-links--share<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>li</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>social-links__item<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>a</span> <span class=\"token attr-name\">href</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>{{ SHARE URL }}<span class=\"token punctuation\">\"</span></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>social-link<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">rel</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>nofollow<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>\n      <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>svg</span><span class=\"token punctuation\">></span></span>{{ SERVICE ICON }}<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>svg</span><span class=\"token punctuation\">></span></span>\n      <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>b</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>social-link__text<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>Share on {{ SERVICE NAME }}<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>b</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>a</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>li</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span></code></pre>\n<p>You can check out the baseline experience on this very page by disabling JavaScript.</p>\n<figure id=\"2023-12-14-01\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2023-12-15/1.png\" alt=\"\"></p>\n<figcaption>My baseline sharing component is a list of icon links.</figcaption>\n</figure>\n<p>It’s worth noting that I have chosen not to enforce opening these links in a new tab. You can do that if you like, but on mobile devices I’d prefer the user just navigate to the share page directly. You may have a different preference, but if you decide to spawn a new tab, be sure your link text lets folks know that’s what will happen. I do include a <a href=\"https://developer.mozilla.org/docs/Web/HTML/Attributes/rel#nofollow\"><code>rel=&quot;nofollow&quot;</code></a> on the link, however, to prevent search spiders from indexing the share forms.</p>\n<p>If you test out these links, you’ll notice many of the target forms will pick up a ton of information from your page automatically. By and large, this info is grabbed from your page’s <a href=\"https://developers.facebook.com/docs/opengraph/\">Open Graph data</a> (<a href=\"https://developers.facebook.com/docs/sharing/webmasters#markup\">stored in <code>meta</code> tags</a>) or <a href=\"https://www.w3.org/TR/json-ld11/\">Linked Data</a> (as <a href=\"https://json-ld.org/\">JSON-LD</a>). You can write that info to your page by hand or use a plugin to generate it for you automatically. There are a ton of options out there if you choose to go the later route (which I’d recommend).</p>\n<h2 id=\"enhancement-level-1%3A-popup-share\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#enhancement-level-1%3A-popup-share\" aria-hidden=\"true\">#</a> Enhancement Level 1: Popup Share</h2>\n<p>If you played around with any of the various share forms, you probably noticed that they are, by and large, designed as discrete interactions best-suited to a narrow window (e.g., mobile) or popup. To provide that experience, I’ve long-relied on a little bit of JavaScript to launch them in a new, appropriately-sized window:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token keyword\">function</span> <span class=\"token function\">popup</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">e</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">var</span> $link <span class=\"token operator\">=</span> e<span class=\"token punctuation\">.</span>target<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">while</span> <span class=\"token punctuation\">(</span>$link<span class=\"token punctuation\">.</span>nodeName<span class=\"token punctuation\">.</span><span class=\"token function\">toLowerCase</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">!=</span> <span class=\"token string\">\"a\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    $link <span class=\"token operator\">=</span> $link<span class=\"token punctuation\">.</span>parentNode<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  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>\n  <span class=\"token keyword\">var</span> popup <span class=\"token operator\">=</span> window<span class=\"token punctuation\">.</span><span class=\"token function\">open</span><span class=\"token punctuation\">(</span>\n    $link<span class=\"token punctuation\">.</span>href<span class=\"token punctuation\">,</span>\n    <span class=\"token string\">\"share\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token string\">\"height=500,width=600,status=no,toolbar=no,popup\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n    popup<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>\n    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>\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span>e<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span><span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">var</span> screen_width <span class=\"token operator\">=</span>\n    <span class=\"token string\">\"visualViewport\"</span> <span class=\"token keyword\">in</span> window\n      <span class=\"token operator\">?</span> window<span class=\"token punctuation\">.</span>visualViewport<span class=\"token punctuation\">.</span>width\n      <span class=\"token operator\">:</span> window<span class=\"token punctuation\">.</span>innerWidth<span class=\"token punctuation\">,</span>\n  $links <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">querySelectorAll</span><span class=\"token punctuation\">(</span><span class=\"token string\">\".social-links--share a\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  count <span class=\"token operator\">=</span> $links<span class=\"token punctuation\">.</span>length<span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>screen_width <span class=\"token operator\">></span> <span class=\"token number\">600</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">while</span> <span class=\"token punctuation\">(</span>count<span class=\"token operator\">--</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    $links<span class=\"token punctuation\">[</span>count<span class=\"token punctuation\">]</span><span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"click\"</span><span class=\"token punctuation\">,</span> popup<span class=\"token punctuation\">,</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    $links<span class=\"token punctuation\">[</span>count<span class=\"token punctuation\">]</span><span class=\"token punctuation\">.</span><span class=\"token function\">querySelector</span><span class=\"token punctuation\">(</span><span class=\"token string\">\".social-link__text\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>innerHTML <span class=\"token operator\">+=</span>\n      <span class=\"token string\">\" (in a popup)\"</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<p>The first chunk defines a new function called <code>popup()</code> that will act as the event listener. It takes the event (<var>e</var>) as an argument and then finds the associated link (bubbling up through the DOM as necessary in that <code>while</code> loop). Once it finds the link, the function opens a new popup window (using <code>window.open()</code>). Then, to check if the popup was blocked, it attempts (within the <code>try…catch</code>) to focus it. If the focus succeeds, which means the popup wasn’t blocked, the script prevents the link from navigating the user to the <code>href</code> (which is the default behavior, hence <code>e.preventDefault()</code>).</p>\n<p>The second block defines a couple of variables we’ll need. First, it captures the current <var>screen_width</var> using either the window’s <code>visualViewport</code> (if available) or its <code>innerWidth</code> (which is more old school). Next it grabs the social links (<var>$links</var>) and counts them for looping purposes (<var>count</var>).</p>\n<p>The final block is a conditional that checks to see if the <var>screen_width</var> is wider than 600px (an arbitrary width that just feels right… your mileage may vary). If the screen is wider than that threshold, it loops through the links,<sup class=\"footnote-ref\"><a href=\"#fn2\" id=\"fnref2\">2</a></sup> adds the click handler, and adds some text to the link label to let folks know it will open a popup.</p>\n<p>And with that, the first layer of enhancement is complete: Users with JavaScript support who also happen to be using a wider browser window will get the popup share form if the popup is allowed. If the popup isn’t allowed, they’ll default to the baseline experience.</p>\n<h2 id=\"enhancement-level-2%3A-os-share\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#enhancement-level-2%3A-os-share\" aria-hidden=\"true\">#</a> Enhancement Level 2: OS Share</h2>\n<p>A few years back, browsers began participating in OS-level share activities. On one side, this allowed websites to share some data—URLs, text, files—to other apps on the device via <code>navigator.share()</code>. On the other side of the equation, Progressive Web Apps could advertise themselves—via the Manifest’s <code>share_target</code> member—as being able to receive content shared in this way.</p>\n<p>Sharing a URL and text is <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share#browser_compatibility\">really well supported</a>. That said, it’s only been around a few years at this point and some browsers require an additional permission to use the API.<sup class=\"footnote-ref\"><a href=\"#fn3\" id=\"fnref3\">3</a></sup> For these reasons, it’s best to use the API as a progressive enhancement. Thankfully, it’s easy to test for support:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token string\">\"share\"</span> <span class=\"token keyword\">in</span> navigator<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// all good!</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<p>For my particular implementation, I’ve decided to swap out the individual links for a single button that, when clicked, will proffer the page’s details over to the OS’s share widget. Here’s the code I use to do that:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token keyword\">var</span> $links <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\">\".social-links--share\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  $parent <span class=\"token operator\">=</span> $links<span class=\"token punctuation\">.</span>parentNode<span class=\"token punctuation\">,</span>\n  $button <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">createElement</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"button\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  title <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\">\"h1.p-name,title\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>innerText<span class=\"token punctuation\">,</span>\n  $description <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">querySelector</span><span class=\"token punctuation\">(</span>\n    <span class=\"token string\">'meta[name=\"og:description\"],meta[name=\"description\"]'</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  text <span class=\"token operator\">=</span> $description <span class=\"token operator\">?</span> $description<span class=\"token punctuation\">.</span><span class=\"token function\">getAttribute</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"content\"</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> <span class=\"token string\">\"\"</span><span class=\"token punctuation\">,</span>\n  url <span class=\"token operator\">=</span> window<span class=\"token punctuation\">.</span>location<span class=\"token punctuation\">.</span>href<span class=\"token punctuation\">;</span>\n\n$button<span class=\"token punctuation\">.</span>innerHTML <span class=\"token operator\">=</span> <span class=\"token string\">\"Share &lt;svg>…&lt;/svg>\"</span><span class=\"token punctuation\">;</span>\n$button<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"click\"</span><span class=\"token punctuation\">,</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">e</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  navigator<span class=\"token punctuation\">.</span><span class=\"token function\">share</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> title<span class=\"token punctuation\">,</span> text<span class=\"token punctuation\">,</span> url <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n$parent<span class=\"token punctuation\">.</span><span class=\"token function\">insertBefore</span><span class=\"token punctuation\">(</span>$button<span class=\"token punctuation\">,</span> $links<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n$links<span class=\"token punctuation\">.</span><span class=\"token function\">remove</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<p>The first block sets up my variables:</p>\n<ul>\n<li><var>$links</var> - A reference to the list (<code>ul</code>) of sharing links;</li>\n<li><var>$parent</var> - the parent container of that list;</li>\n<li><var>$button</var> - the button I’m going to swap in for the links;</li>\n<li><var>title</var> - The page title (either from the page’s <code>h1</code> or <code>title</code> element);</li>\n<li><var>$description</var> - A reference to a <code>meta</code> description element;</li>\n<li><var>text</var> - The text content of that description, if one is found; and</li>\n<li><var>url</var> - The URL to be shared.</li>\n</ul>\n<p>The second block sets up the button by inserting the text “Share” and an SVG share icon and setting an event listener on it that will pass the collected info to <code>navigator.share()</code>.</p>\n<p>The third and final block swaps out the link list for the button.</p>\n<h2 id=\"putting-it-all-together\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#putting-it-all-together\" aria-hidden=\"true\">#</a> Putting It All Together</h2>\n<p>The final step to putting this all together involves setting up the conditional that determines which enhancement is offered. To keep everything a bit cleaner, I’m also moving each of the enhancements into its own function:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token operator\">!</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">window<span class=\"token punctuation\">,</span> document<span class=\"token punctuation\">,</span> navigator</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">function</span> <span class=\"token function\">prepForPopup</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">// popup code</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">function</span> <span class=\"token function\">popup</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">// popup event handler</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">function</span> <span class=\"token function\">swapForShareAPI</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">// share button code</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token string\">\"share\"</span> <span class=\"token keyword\">in</span> navigator<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token function\">swapForShareAPI</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">else</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token function\">prepForPopup</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">this</span><span class=\"token punctuation\">,</span> <span class=\"token keyword\">this</span><span class=\"token punctuation\">.</span>document<span class=\"token punctuation\">,</span> <span class=\"token keyword\">this</span><span class=\"token punctuation\">.</span>navigator<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<p>With this setup in place, I can provide the optimal experience in browsers that support the web share API and a pretty decent fallback experience to browsers that don’t. And if none of these enhancements can be applied, users can still share my content to the places I’ve identified… no cookies or third-party widgets required.</p>\n<p>You can <a href=\"https://codepen.io/aarongustafson/pen/eYxajwy\">see (and play with) an isolated demo of this interface over on Codepen</a>.</p>\n<hr class=\"footnotes-sep\">\n<section class=\"footnotes\">\n<h4 class=\"hidden\">Footnotes</h4>\n<ol class=\"footnotes-list\">\n<li id=\"fn1\" class=\"footnote-item\"><p>Interesting side-note: If you own a form like this on your site, <a href=\"/notebook/my-own-personal-pwa/\">it makes a great share target</a>. <a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n<li id=\"fn2\" class=\"footnote-item\"><p>Why a reverse <code>while</code> loop? Well, the order of execution doesn’t matter and decrementing <code>while</code> loops are faster in some instances. It’s a micro-optimization that boosts perf on older browsers and lower-end chipsets. <a href=\"#fnref2\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n<li id=\"fn3\" class=\"footnote-item\"><p>Like many modern APIs, it also requires a secure connection (HTTPS). <a href=\"#fnref3\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n</ol>\n</section>\n","social_text":"Are you still relying on third party share widgets? You should really stop that. Here’s how…","url":"https://www.aaron-gustafson.com/notebook/sharing-in-the-age-of-3p-cookie-mageddon/","tags":["HTML","privacy","progressive enhancement","the web","user experience","web development"],"image":"https://www.aaron-gustafson.com/i/posts/2023-12-15/hero.jpg","date_published":"2023-12-15T18:28:35Z"},{"id":"https://www.aaron-gustafson.com/notebook/widgets/","title":"✍🏻 Widgets!","excerpt":"<p>It was a long time coming, but I finally had a chance to put the work I did on <a href=\"https://github.com/MicrosoftEdge/MSEdgeExplainers/tree/main/PWAWidgets\">a widgets proposal for PWAs</a> into practice on my own site. I’m pretty excited about it!</p>","summary":"<p>It was a long time coming, but I finally had a chance to put the work I did on <a href=\"https://github.com/MicrosoftEdge/MSEdgeExplainers/tree/main/PWAWidgets\">a widgets proposal for PWAs</a> into practice on my own site. I’m pretty excited about it!</p>","content_html":"<p>It was a long time coming, but I finally had a chance to put the work I did on <a href=\"https://github.com/MicrosoftEdge/MSEdgeExplainers/tree/main/PWAWidgets\">a widgets proposal for PWAs</a> into practice on my own site. I’m pretty excited about it!</p>\n<h2 id=\"where-it-all-started\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#where-it-all-started\" aria-hidden=\"true\">#</a> Where it all started</h2>\n<p>I had <a href=\"https://web.archive.org/web/20200929174844/https://discourse.wicg.io/t/noodling-on-an-idea-projections-for-web-apps/3900\">the original idea for “projections”</a> way back in 2019. Inspired by <a href=\"https://en.wikipedia.org/wiki/Dashboard_(macOS)#Widget_functions_and_capabilities\">OS X’s Dashboard Widgets</a> and <a href=\"https://en.wikipedia.org/wiki/Adobe_AIR\">Adobe AIR</a>, I’d begun to wonder if it might be possible to <em>project</em> a component from a website into those kinds of surfaces. Rather than building a bespoke widget that connected to an API, I thought it made sense to leverage an installed PWA to manage those “projections.” I shared the idea at TPAC that year and got some interest from a broad range of folks, but didn’t have much time to work on the details until a few years later.</p>\n<p>In the intervening time, I kept working through the concept in my head. I mean in an ideal world, the widget would just be a responsive web page, right? But if that were the case, what happens when every widget loads the entirety of React to render their stock ticker? That seemed like a performance nightmare.</p>\n<p>In my gut, I felt like the right way to build things would be to have a standard library of widget templates and to enable devs to flow data into them via a Service Worker. Alex Russell suggested I model the APIs on how Notifications are handled (since they serve a similar function) and I was off to the races.</p>\n<p>I drafted <a href=\"https://github.com/aarongustafson/pwa-widgets\">a substantial proposal for my vision of how PWA widgets should work</a>. Key aspects included:</p>\n<ul>\n<li>A declarative way to define and configure a widget from within the Web App Manifest;</li>\n<li>A progressively enhanced pathway for devs to design a widget that adapts to its host environment, from using predefined templates to using custom templates to full-blown web-based widgets (with rendering akin to an <code>iframe</code>);</li>\n<li>A collection of recommended stock templates that implementors should offer to support most widget types;</li>\n<li>Extensibility to support custom templates using any of a variety of templating languages; and</li>\n<li>A complete suite of tools for managing widgets and any associated business logic within a Service Worker.</li>\n</ul>\n<h2 id=\"widgets-became-a-reality\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#widgets-became-a-reality\" aria-hidden=\"true\">#</a> Widgets became a reality</h2>\n<p>After continuing to gently push on this idea with colleagues across Microsoft (and beyond), I discovered that the Windows 11 team was looking to open up the new Widget Dashboard to third-party applications. I saw this as an opportunity to turn my idea into a reality. After working my way into the conversation, I made a solid case for why PWAs needed to be a part of that story and… it worked! (It no doubt helped that companies including Meta, Twitter, and Hulu were all invested in PWA as a means of delivering apps for Windows.)</p>\n<p>While the timeline for implementation didn’t allow us to tackle the entirety of my proposal, we did carve out the pieces that made for a compelling MVP. This allowed us to show what’s possible, see how folks use it, and plan for future investment in the space.</p>\n<p>Sadly, it meant tabling two features I really loved:</p>\n<ul>\n<li><strong>Stock/predefined templates</strong>. A library of lightly theme-able, consistent, cross-platform templates based on common data structures (e.g., RSS/Atom, iCal) would make it incredibly simple for devs to build a widget. If implemented well, devs might not even need to write a single line of business logic in their Service Worker as the browser could pick up all of the configuration details from the Manifest.</li>\n<li><strong>Configurable widget instances.</strong> Instead of singleton widgets, these would allow you to define a single widget type and replicate it for different use cases. For example, a widget to follow a social media user’s profile could be defined once and the individual instances could be configured with the specific account to be followed.</li>\n</ul>\n<p>I’m sincerely hopeful these two features eventually make their way to us as I think they truly unlock the power of the widget platform. Perhaps, with enough uptake on the current implementation, we can revisit these in the not-too-distant future.</p>\n<hr>\n<p>To test things out, I decided to build two widgets for this site:</p>\n<ol>\n<li>Latest posts</li>\n<li>Latest links</li>\n</ol>\n<p>Both are largely the same in terms of their setup: They display a list of linked titles from this site.</p>\n<h2 id=\"designing-my-widget-templates\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#designing-my-widget-templates\" aria-hidden=\"true\">#</a> Designing my widget templates</h2>\n<p>Given that they were going to be largely identical, I made <a href=\"https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/w/feed.ac.json\">a single “feed” template for use in both widgets</a>. The templating tech I used is called <a href=\"https://adaptivecards.io\">Adaptive Cards</a>, which is what Windows 11 uses for rendering.</p>\n<p>Adaptive Card templates are relatively straightforward JSON:</p>\n<pre class=\"language-json\" tabindex=\"0\"><code class=\"language-json\">&amp;#<span class=\"token number\">123</span>;\n  <span class=\"token property\">\"type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"AdaptiveCard\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"$schema\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"http://adaptivecards.io/schemas/adaptive-card.json\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"version\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"1.6\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"body\"</span><span class=\"token operator\">:</span> &amp;#<span class=\"token number\">91</span>;\n    &amp;#<span class=\"token number\">123</span>;\n      <span class=\"token property\">\"$data\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"$&amp;#123;take(items,5)&amp;#125;\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Container\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"items\"</span><span class=\"token operator\">:</span> &amp;#<span class=\"token number\">91</span>;\n        &amp;#<span class=\"token number\">123</span>;\n          <span class=\"token property\">\"type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"TextBlock\"</span><span class=\"token punctuation\">,</span>\n          <span class=\"token property\">\"text\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"&amp;#91;$&amp;#123;title&amp;#125;&amp;#93;($&amp;#123;url&amp;#125;)\"</span><span class=\"token punctuation\">,</span>\n          <span class=\"token property\">\"wrap\"</span><span class=\"token operator\">:</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">,</span>\n          <span class=\"token property\">\"weight\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Bolder\"</span><span class=\"token punctuation\">,</span>\n          <span class=\"token property\">\"spacing\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Padding\"</span><span class=\"token punctuation\">,</span>\n          <span class=\"token property\">\"height\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"stretch\"</span>\n        &amp;#<span class=\"token number\">125</span>;\n      &amp;#<span class=\"token number\">93</span>;<span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"height\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"stretch\"</span>\n    &amp;#<span class=\"token number\">125</span>;\n  &amp;#<span class=\"token number\">93</span>;<span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"backgroundImage\"</span><span class=\"token operator\">:</span> &amp;#<span class=\"token number\">123</span>;\n    <span class=\"token property\">\"url\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"https://www.aaron-gustafson.com/i/background-logo.png\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token property\">\"verticalAlignment\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Bottom\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token property\">\"horizontalAlignment\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Center\"</span>\n  &amp;#<span class=\"token number\">125</span>;\n&amp;#<span class=\"token number\">125</span>;</code></pre>\n<p>What this structure does is:</p>\n<ol>\n<li>Create a container into which I will place the content;</li>\n<li>Extract the first five <code>items</code> from the data being fed into the template (more on that in a moment);</li>\n<li>Loop through each <code>item</code> and\n<ul>\n<li>create a text block,</li>\n<li>populate its content with Markdown to generate a linked title (using the <code>title</code> and <code>url</code> keys from the <code>item</code> object)</li>\n<li>Set some basic styles to make the text bold, separate the titles a little and make them grow to fill the container; then, finally</li>\n</ul>\n</li>\n<li>Set a background on the widget.</li>\n</ol>\n<p>The way Adaptive Cards work is that they flow JSON data into a template and render that. The variable names in the template map directly to the incoming data structure, so are totally up to you to define. As these particular widgets are feed-driven and <a href=\"https://www.aaron-gustafson.com/feeds/\">this site already supports JSONFeed</a>, I set up the widgets to flow the appropriate feed into each and used the keys that were already there. For reference, here’s a sample JSONFeed <code>item</code>:</p>\n<pre class=\"language-json\" tabindex=\"0\"><code class=\"language-json\"><span class=\"token punctuation\">{</span>\n  <span class=\"token property\">\"id\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"…\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"title\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"…\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"summary\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"…\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"content_html\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"…\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"url\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"…\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"tags\"</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"date_published\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"…\"</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<p>If you want to tinker with Adaptive Cards and make your own, you can do so with <a href=\"https://adaptivecards.io/designer/\">their Designer tool</a>.</p>\n<h3 id=\"defining-the-widgets-in-the-manifest\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#defining-the-widgets-in-the-manifest\" aria-hidden=\"true\">#</a> Defining the widgets in the Manifest</h3>\n<p>With a basic template created, the next step was to set up the two widgets in my Manifest. As they both function largely the same, I’ll just focus on the definition for one of them.</p>\n<p>First off, defining widgets in the Manifest is done via the <code>widgets</code> member, which is an array (much like <code>icons</code> and <code>shortcuts</code>). Each widget is represented as an object in that array. Here is the definition for the “latest posts” widget:</p>\n<pre class=\"language-json\" tabindex=\"0\"><code class=\"language-json\"><span class=\"token punctuation\">{</span>\n  <span class=\"token property\">\"name\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Latest Posts\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"short_name\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"Posts\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"tag\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"feed-posts\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"description\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"The latest posts from Aaron Gustafson’s blog\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"template\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"feed\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"ms_ac_template\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"/w/feed.ac.json\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"data\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"/feeds/latest-posts.json\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"application/json\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"auth\"</span><span class=\"token operator\">:</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"update\"</span><span class=\"token operator\">:</span> <span class=\"token number\">21600</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"icons\"</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n    <span class=\"token punctuation\">{</span>\n      <span class=\"token property\">\"src\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"/i/icons/webicon-rss.png\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"type\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"image/png\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"sizes\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"120x120\"</span>\n    <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n  <span class=\"token property\">\"screenshots\"</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>\n    <span class=\"token punctuation\">{</span>\n      <span class=\"token property\">\"src\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"/i/screenshots/widget-posts.png\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"sizes\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"387x387\"</span><span class=\"token punctuation\">,</span>\n      <span class=\"token property\">\"label\"</span><span class=\"token operator\">:</span> <span class=\"token string\">\"The latest posts widget\"</span>\n    <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">]</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<p>Breaking this down:</p>\n<ol>\n<li><code>name</code> and <code>short_name</code> act much like these keys in the root of the Manifest as well as in <code>shortcuts</code>: The <code>name</code> value is used as the name for the widget unless there’s not enough room, in which case <code>short_name</code> is used.</li>\n<li>You can think of <code>tag</code> as analogous to <code>class</code> in HTML sense. It’s a way of labeling a widget so you can easily reference it later. Each widget instance will have a unique id created by the widget service, but that instance (or all instances, if the widget supports multiple instances) can be accessed via the <code>tag</code>. But more on that later.</li>\n<li>The <code>description</code> key is used for marketing the widget within a host OS or digital storefront. It should accurately (and briefly) describe what the widget does.</li>\n<li>The <code>template</code> key is not currently used in the Windows 11 implementation but refers to the expected standard library widget template provided by the system. As a template library is not currently available, the <code>ms_ac_template</code> value is used to provide a URL to get the custom Adaptive Card (hence “ac”) template. The “ms_” prefix is there because it’s expected that this would be a Microsoft-proprietary property. It follows <a href=\"https://www.w3.org/TR/appmanifest/#proprietary-extensions\">the guidance for extending the Manifest</a>.</li>\n<li>The <code>data</code> and <code>type</code> keys define the path to the data that should be fed into the template for rendering by the widget host and the MIME of the data format it’s in. The Windows 11 implementation currently only accepts JSON data, but the design of widgets is set up to allow for this to eventually extend to other standardized formats like RSS, iCal, vCard, and such.</li>\n<li><code>update</code> is an optional configuration member allowing you to set how often you’d like the widget to update, in seconds. Developers currently need to add the logic for implementing this into their Service Worker, but this setup allows the configuration to remain independent of the JavaScript code, making it easier to maintain.</li>\n<li>Finally, <code>icons</code> and <code>screenshots</code> allow us to define how the widget shows up in the widget host and how it is promoted for install.</li>\n</ol>\n<p>When someone installs my site as a PWA, the information about the available widgets gets ingested by the browser. The browser then determines, based on the provided values and its knowledge of the available widget service(s) on the device, which widgets should be offered. On Windows 11, this information is <a href=\"https://learn.microsoft.com/en-us/windows/apps/develop/widgets/implement-widget-provider-cs#update-the-package-manifest\">routed into the AppXManifest that governs how apps are represented in Windows</a>. The Windows 11 widget service can then read in the details about the available widgets and offer them for users to install.</p>\n<figure id=\"2023-10-09-01\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2023-10-09/widgets-promotion.gif\" alt=\"\"></p>\n<figcaption>An animated capture of Windows 11’s widget promotion surface, showing 2 widgets available from this site’s PWA.</figcaption>\n</figure>\n<h2 id=\"adding-widget-support-to-my-service-worker\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#adding-widget-support-to-my-service-worker\" aria-hidden=\"true\">#</a> Adding widget support to my Service Worker</h2>\n<p>As I mentioned earlier, all of the plumbing for widgets is done within a Service Worker and is modeled on the Notifications API. I’m not going to exhaustively detail how it all works, but I’ll give you enough detail to get you started.</p>\n<p>First off, widgets are exposed via the <code>self.widgets</code> interface. Most importantly, this interface lets you access and update any instances of a widget connected to your PWA.</p>\n<h3 id=\"installing-a-widget\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#installing-a-widget\" aria-hidden=\"true\">#</a> Installing a widget</h3>\n<p>When a user chooses to install a widget, that emits a “widgetinstall” event in your Service Worker. You use that to kickoff the widget lifecycle by gathering the template and data needed to instantiate the widget:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\">self<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"widgetinstall\"</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\">=></span> <span class=\"token punctuation\">{</span>\n  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\">`</span><span class=\"token string\">Installing </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>event<span class=\"token punctuation\">.</span>widget<span class=\"token punctuation\">.</span>tag<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  event<span class=\"token punctuation\">.</span><span class=\"token function\">waitUntil</span><span class=\"token punctuation\">(</span><span class=\"token function\">initializeWidget</span><span class=\"token punctuation\">(</span>event<span class=\"token punctuation\">.</span>widget<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<p>The event argument comes in with details of the specific widget being instantiated (as <code>event.widget</code>). In the code above, you can see I’ve logged the widget’s <code>tag</code> value to the console. I pass the widget information over to my <code>initializeWidget()</code> function and it updates the widget with the latest data and, if necessary, sets up a <a href=\"https://developer.mozilla.org/docs/Web/API/Web_Periodic_Background_Synchronization_API\">Periodic Background Sync</a>:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">initializeWidget</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">widget</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">await</span> <span class=\"token function\">updateWidget</span><span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">await</span> <span class=\"token function\">registerPeriodicSync</span><span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">return</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<p>The code for my <code>updateWidget()</code> function is as follows:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">updateWidget</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">widget</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> template <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">await</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">.</span>msAcTemplate<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">text</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> data <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">await</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">.</span>data<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">text</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">try</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">await</span> self<span class=\"token punctuation\">.</span>widgets<span class=\"token punctuation\">.</span><span class=\"token function\">updateByTag</span><span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">.</span>tag<span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> template<span class=\"token punctuation\">,</span> data <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span> <span class=\"token keyword\">catch</span> <span class=\"token punctuation\">(</span>e<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    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\">`</span><span class=\"token string\">Couldn’t update the widget </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>tag<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span> e<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">return</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<p>This function does the following:</p>\n<ol>\n<li>Get the template for this widget</li>\n<li>Get the data to flow into the template</li>\n<li>Use the <code>self.widgets.updateByTag()</code> method to push the <var>template</var> and <var>data</var> to the widget service to update any widget instances connected to the widget’s <code>tag</code>.</li>\n</ol>\n<p>As I mentioned, I also have code in place to take advantage of Periodic Background Sync if/when it’s available and the browser allows my site to do it:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">registerPeriodicSync</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">widget</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">let</span> tag <span class=\"token operator\">=</span> widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">.</span>tag<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token string\">\"update\"</span> <span class=\"token keyword\">in</span> widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    registration<span class=\"token punctuation\">.</span>periodicSync<span class=\"token punctuation\">.</span><span class=\"token function\">getTags</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">then</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">tags</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n      <span class=\"token comment\">// only one registration per tag</span>\n      <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>tags<span class=\"token punctuation\">.</span><span class=\"token function\">includes</span><span class=\"token punctuation\">(</span>tag<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n        periodicSync<span class=\"token punctuation\">.</span><span class=\"token function\">register</span><span class=\"token punctuation\">(</span>tag<span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n          <span class=\"token literal-property property\">minInterval</span><span class=\"token operator\">:</span> widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">.</span>update<span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n      <span class=\"token punctuation\">}</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">return</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<p>This function also receives the widget details and:</p>\n<ol>\n<li>Looks to see if the widget <code>definition</code> (from the Manifest) includes an <code>update</code> member. If it has one, it…</li>\n<li>Checks to see if there’s already a Periodic Background Sync that is registered for this tag. If none exists, it…</li>\n<li>Registers a new Periodic Background Sync using the <code>tag</code> value and a minimum interval equal to the <code>update</code> requested.</li>\n</ol>\n<p>The <code>update</code> member, as you may recall, is the frequency (in seconds) you’d ideally like the widget to be updated. In reality, you’re at the mercy of the browser as to when (or even if) your sync will run, but that’s totally cool as there are other ways to update widgets as well.<sup class=\"footnote-ref\"><a href=\"#fn1\" id=\"fnref1\">1</a></sup></p>\n<h3 id=\"uninstalling-a-widget\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#uninstalling-a-widget\" aria-hidden=\"true\">#</a> Uninstalling a widget</h3>\n<p>When a user uninstalls a widget, your Service Worker will receive a “widgetuninstall” event. Much like the “widgetinstall” event, the argument contains details about that widget which you can use to clean up after yourself:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\">self<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"widgetuninstall\"</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\">=></span> <span class=\"token punctuation\">{</span>\n  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\">`</span><span class=\"token string\">Uninstalling </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>event<span class=\"token punctuation\">.</span>widget<span class=\"token punctuation\">.</span>tag<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  event<span class=\"token punctuation\">.</span><span class=\"token function\">waitUntil</span><span class=\"token punctuation\">(</span><span class=\"token function\">uninstallWidget</span><span class=\"token punctuation\">(</span>event<span class=\"token punctuation\">.</span>widget<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<p>Your application may have different cleanup needs, but this is a great time to clean up any unneeded Periodic Sync registrations. Just be sure to check the length of the widget’s <code>instances</code> array (<code>widget.instances</code>) to make sure you’re dealing with the last instance of a given widget <em>before</em> you unregister the sync:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\"><span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">uninstallWidget</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">widget</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">.</span>instances<span class=\"token punctuation\">.</span>length <span class=\"token operator\">===</span> <span class=\"token number\">1</span> <span class=\"token operator\">&amp;&amp;</span> <span class=\"token string\">\"update\"</span> <span class=\"token keyword\">in</span> widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">await</span> self<span class=\"token punctuation\">.</span>registration<span class=\"token punctuation\">.</span>periodicSync<span class=\"token punctuation\">.</span><span class=\"token function\">unregister</span><span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">.</span>definition<span class=\"token punctuation\">.</span>tag<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">return</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre>\n<h3 id=\"refreshing-your-widgets\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#refreshing-your-widgets\" aria-hidden=\"true\">#</a> Refreshing your widgets</h3>\n<p>Widget platforms may periodically freeze your widget(s) to save resources. For example, they may do this when widgets are not visible. To keep your widgets up to date, they will periodically issue a “widgetresume” event. If you’ve modeled your approach on the one I’ve outlined above, you can route this event right through to your <code>updateWidget()</code> function:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\">self<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"widgetresume\"</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\">=></span> <span class=\"token punctuation\">{</span>\n  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\">`</span><span class=\"token string\">Resuming </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>event<span class=\"token punctuation\">.</span>widget<span class=\"token punctuation\">.</span>tag<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  event<span class=\"token punctuation\">.</span><span class=\"token function\">waitUntil</span><span class=\"token punctuation\">(</span><span class=\"token function\">updateWidget</span><span class=\"token punctuation\">(</span>event<span class=\"token punctuation\">.</span>widget<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<h3 id=\"actions\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#actions\" aria-hidden=\"true\">#</a> Actions</h3>\n<p>While I don’t want to get too into the weeds here, I do want to mention that widgets can have predefined user actions as well. These actions result in “widget click” events being sent back to the Service Worker so you can respond to them:</p>\n<pre class=\"language-js\" tabindex=\"0\"><code class=\"language-js\">self<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"widgetclick\"</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\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> widget <span class=\"token operator\">=</span> event<span class=\"token punctuation\">.</span>widget<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> action <span class=\"token operator\">=</span> event<span class=\"token punctuation\">.</span>action<span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">switch</span> <span class=\"token punctuation\">(</span>action<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">// Custom Actions</span>\n    <span class=\"token keyword\">case</span> <span class=\"token string\">\"refresh\"</span><span class=\"token operator\">:</span>\n      event<span class=\"token punctuation\">.</span><span class=\"token function\">waitUntil</span><span class=\"token punctuation\">(</span><span class=\"token function\">updateWidget</span><span class=\"token punctuation\">(</span>widget<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n      <span class=\"token keyword\">break</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre>\n<p>For a great example of how a widget can integrate actions, you should check out <a href=\"https://microsoftedge.github.io/Demos/pwamp/\">the demo PWAmp project</a>. <a href=\"https://github.com/MicrosoftEdge/Demos/blob/main/pwamp/sw-widgets.js\">Their Service Worker widget code</a> is worth a read.</p>\n<h2 id=\"result!\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#result!\" aria-hidden=\"true\">#</a> Result!</h2>\n<p>With all of these pieces in place, I was excited to see my site showing up in the Widget Dashboard in Windows 11.</p>\n<figure id=\"2023-10-09-02\">\n<p><img src=\"https://www.aaron-gustafson.com/i/posts/2023-10-09/widgets-in-windows.jpg\" alt=\"\"></p>\n<figcaption>A screenshot of Windows 11 showing the Widget Dashboard overlaying the desktop with this site installed as a PWA to the right. The “latest posts” and “latest links” widgets are shown.</figcaption>\n</figure>\n<p>You can view the full source code on GitHub:</p>\n<ul>\n<li><a href=\"https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/w/feed.ac.json\">“Feed” Adaptive Card Template</a></li>\n<li><a href=\"https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/manifest.json#L158-L237\">Widget definitions in the Manifest</a></li>\n<li><a href=\"https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/_javascript/serviceworker/widgets.js\">Widgets code in my Service Worker</a></li>\n</ul>\n<hr>\n<p>I’m quite hopeful this will be the first of many places PWA-driven widgets will appear. If you’s like to see them supported elsewhere, be sure to tell your browser and OS vendor(s) of choice. The more they hear from their user base that this feature is needed, the more likely we are to see it get implemented in more places.</p>\n<h2 id=\"addendum%3A-gotchas\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"#addendum%3A-gotchas\" aria-hidden=\"true\">#</a> Addendum: Gotchas</h2>\n<p>In wiring this all up, I ran into a few current bugs I wanted to flag so you can avoid them:</p>\n<ul>\n<li>The <code>icons</code> member won’t accept SVG images. This should eventually be fixed, but it was keeping my widgets from appearing as installable.</li>\n<li>The <code>screenshots</code> members can’t be incredibly large. I’m told you should provide square screenshots no larger than 500px ×500px.</li>\n</ul>\n<hr class=\"footnotes-sep\">\n<section class=\"footnotes\">\n<h4 class=\"hidden\">Footnotes</h4>\n<ol class=\"footnotes-list\">\n<li id=\"fn1\" class=\"footnote-item\"><p>Have you checked out <a href=\"https://developer.mozilla.org/docs/Web/API/Server-sent_events/Using_server-sent_events\">Server Events</a>? <a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n</ol>\n</section>\n","social_text":"I finally had a chance to put the work I did on a widgets proposal for PWAs into practice on my own site. It’s pretty exciting!","url":"https://www.aaron-gustafson.com/notebook/widgets/","tags":["progressive web apps","experiments","JavaScript","Microsoft","this site","user experience","web development","web standards","Windows"],"image":"https://www.aaron-gustafson.com/i/posts/2023-10-09/hero1.jpg","date_published":"2023-10-09T22:38:54Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/assume-the-position-a-labelling-story/","title":"🔗 Assume the Position—A Labelling Story","content_html":"<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>\n","social_text":"Interesting examination of label positioning relative to checkboxes and radio controls in forms. While ostensibly web-focused, it applies equally to any GUI.","url":"https://www.aaron-gustafson.com/notebook/links/assume-the-position-a-labelling-story/","external_url":"https://www.tpgi.com/assume-the-position-a-labelling-story/","tags":["accessibility","forms","design","user experience"],"image":"https://www.tpgi.com/wp-content/uploads/labelling-should-be.png","date_published":"2023-06-07T20:40:33Z"},{"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-/","title":"🔗 Checkout Usability: Autodetect “City” and “State” Inputs Based on the User’s Postal Code (28% of Mobile Sites Don’t)","content_html":"<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>\n","social_text":"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!","url":"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-/","external_url":"https://baymard.com/blog/zip-code-auto-detection?utm_medium=email&utm_campaign=Article%20Checkout%20Usability%20Autodetect%20City%20and%20State%20Inputs%20Based%20on%20the%20Users%20Postal%20Code%2028%20of%20Mobile%20Sites%20Dont&utm_content=Article%20Checkout%20Usability%20Autodetect%20City%20and%20State%20Inputs%20Based%20on%20the%20Users%20Postal%20Code%2028%20of%20Mobile%20Sites%20Dont+CID_06e39fd9939f69a009f7b4d63d1e0da8&utm_source=CampaignMonitor&utm_term=read%20the%20article","tags":["progressive enhancement","forms","user experience"],"image":"https://cdn.baymard.com/research/media_files/attachments/83684/original/research-media-file-37a02b5a04643939d772c093c9aeb1eb.jpg","date_published":"2023-04-12T23:01:09Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/how-ux-designers-can-engage-their-imagination/","title":"🔗 How UX designers can engage their imagination","content_html":"<p>So much worthy of reflecting on on this piece!</p>\n<blockquote>\n<p>I do not believe you can codesign your way to justice.</p>\n<p>Certain institutions and design ideas are fundamentally oppressive, and the only way to achieve radical transformation at scale is with collective action and policy change.</p>\n</blockquote>\n<p>Imagination is key, but imagination in the right way:</p>\n<blockquote>\n<p>Imagination is not a splashy poster of a sci-fi movie but a daily act of resistance we must engage in despite how tired we might be.</p>\n</blockquote>\n<p>Following this framing, author Alba Villamil walls through a ton of actionable above and examples of how to bring imagination to hear on our UX work. Well worth your time!</p>\n","social_text":"Drop much goodness in this piece! “Imagination is not a splashy poster of a sci-fi movie but a daily act of resistance we must engage in despite how tired we might be.”","url":"https://www.aaron-gustafson.com/notebook/links/how-ux-designers-can-engage-their-imagination/","external_url":"https://www.fastcompany.com/90846822/ux-designers-suffering-from-failure-of-imagination","tags":["user experience","inclusive design"],"image":"https://images.fastcompany.net/image/upload/w_1280,f_auto,q_auto,fl_lossy/wp-cms/uploads/2023/02/p-1-90846822-ux-ethics.jpg","date_published":"2023-02-18T20:11:12Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/digital-exclusion-in-healthcare-how-to-change-it/","title":"🔗 Digital Exclusion in Healthcare &amp;amp; How to Change It","summary":"<p>Fantastic talk from Sareh on assumptions we make about our users and how those assumptions exclude people who have different lived experiences than we do.</p>","content_html":"<p>Fantastic talk from Sareh on assumptions we make about our users and how those assumptions exclude people who have different lived experiences than we do. Her focus is on digital healthcare, but is applicable to everything.</p>\n<p><a href=\"https://www.youtube.com/watch?v=Zi1NXGgsM3s\">https://www.youtube.com/watch?v=Zi1NXGgsM3s</a></p>\n<p>I love her calls to action as well!</p>\n<p>Related talk: <a href=\"https://presentations.aaron-gustafson.com/nqpS67/delivering-critical-information-services\">Delivering Critical Information &amp; Services</a></p>\n","social_text":"Amazing talk from @Sareh88 on assumptions we make about our users and how those assumptions exclude people who have different lived experiences than we do 🥰","url":"https://www.aaron-gustafson.com/notebook/links/digital-exclusion-in-healthcare-how-to-change-it/","external_url":"https://www.youtube.com/watch?v=Zi1NXGgsM3s","tags":["accessibility","empathy","inclusive design","industry","user experience"],"date_published":"2023-01-27T17:17:39Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/disruptive-design-patterns-an-uncharted-territory/","title":"🔗 Disruptive design patterns — an uncharted territory","content_html":"<p>Excellent advice here:</p>\n<blockquote>\n<p>[N]ext time you’re designing a new interface paradigm or chatting with an engineer, ask yourself about the risks involved in the known versus the unknown with the following questions.</p>\n<ul>\n<li>Does the new design use intuitive patterns that prioritize consistency?</li>\n<li>Are you in any way disregarding accessibility practices in favor of a feature or a visual direction?</li>\n<li>How tech-savvy are your users and can the newly-introduced experience be easily adopted by current and future, more-diverse audiences?</li>\n<li>Can and will your design decisions be validated through properly conducted user research and user testing?</li>\n</ul>\n<p>Being mindful of these practices will help you guide decisions and ensure you don’t change things just because you can.</p>\n</blockquote>\n","social_text":"Good examination of “disruptive” design patterns and how to consider the impact prior to taking them live.","url":"https://www.aaron-gustafson.com/notebook/links/disruptive-design-patterns-an-uncharted-territory/","external_url":"https://uxdesign.cc/disruptive-design-patterns-an-uncharted-territory-c1a857f2ff93","tags":["user experience"],"image":"https://miro.medium.com/max/1200/0*DqZGJjkCmJ9H9c8M","date_published":"2022-11-02T22:53:21Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/designing-better-error-messages-ux/","title":"🔗 Designing Better Error Messages UX","content_html":"<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>\n","social_text":"Tons of great info in this @smashingmag piece on the UX of error messages.","url":"https://www.aaron-gustafson.com/notebook/links/designing-better-error-messages-ux/","external_url":"https://www.smashingmagazine.com/2022/08/error-messages-ux-design/","tags":["forms","user experience","accessibility"],"image":"https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/93af7c7e-6484-4f47-9e79-fcf6aa24b3ce/better-error-messages-ux.jpg","date_published":"2022-08-30T16:56:41Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/why-everyone-is-watching-tv-with-closed-captioning-on-these-days/","title":"🔗 Why Everyone Is Watching TV with Closed Captioning On These Days","content_html":"<p>Subtitles are yet another example of an accessibility feature that improves the experience of a much broader audience.</p>\n<p>Personally, I started using closed captions when we came home with Oscar. It also me to watch movie &amp; shows while he slept in my arms. Interestingly, I’ve always tuned in dialogue subtitles in video games.</p>\n","social_text":"Subtitles are yet another example of an accessibility feature that improves the experience of a much broader audience.","url":"https://www.aaron-gustafson.com/notebook/links/why-everyone-is-watching-tv-with-closed-captioning-on-these-days/","external_url":"https://kottke.org/19/04/why-everyone-is-watching-tv-with-closed-captioning-on-these-days","tags":["accessibility","user experience"],"image":"https://kottke.org/plus/misc/images/ice-law-order.jpg","date_published":"2019-04-27T14:43:51Z"},{"id":"https://www.aaron-gustafson.com/notebook/links/policymakers-sound-alarm-on-dark-patterns-deceptive-web-design-tricks/","title":"🔗 Policymakers Sound Alarm On “Dark Patterns,” Deceptive Web Design Tricks","content_html":"<p>The <a href=\"https://www.scribd.com/document/405606873/Detour-Act-Final\">DETOUR Act</a>, introduced by Sens. Mark Warner and Deb Fischer, targets bad actors on the web. I need to read through it fully to get a sense of what covered and/or missing, but that this is happening is, I think, a good thing.</p>\n","url":"https://www.aaron-gustafson.com/notebook/links/policymakers-sound-alarm-on-dark-patterns-deceptive-web-design-tricks/","external_url":"https://www.ndtv.com/world-news/policymakers-sound-alarm-on-dark-patterns-deceptive-web-design-trick-2020659","tags":["privacy","user experience"],"image":"https://c.ndtvimg.com/2019-04/4idbju0o_patterns650_625x300_10_April_19.jpg","date_published":"2019-04-10T15:18:25Z"}]}