<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/css" href="https://www.aaron-gustafson.com/c/feed.min.css" ?><feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:amg="https://www.aaron-gustafson.com.com/amg-dtd/"><title>Aaron Gustafson: Latest Posts</title><subtitle>The latest 20 posts.</subtitle><id>https://www.aaron-gustafson.com</id><link href="https://www.aaron-gustafson.com/feeds/latest-posts.xml" rel="self"/><link href="https://www.aaron-gustafson.com"/><author><name>Aaron Gustafson</name><uri>https://www.aaron-gustafson.com</uri></author><updated>2026-05-29T09:00:00Z</updated><entry><id>https://www.aaron-gustafson.com/notebook/fixing-accessibility-after-the-fact-is-too-late/</id><title type="html"><![CDATA[Fixing Accessibility After the Fact Is Too Late]]></title><link href="https://www.aaron-gustafson.com/notebook/fixing-accessibility-after-the-fact-is-too-late/" rel="alternate" type="text/html" /><published>2026-05-29T09:00:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>At Ability Summit 2026, I had the pleasure of moderating a panel on a topic that feels more urgent by the day: how we prevent accessibility defects before they are baked into AI-assisted workflows and shipped at scale.</p><blockquote><p>As AI accelerates how software and content are created, accessibility risks emerge earlier and propagate faster than traditional testing and remediation can handle. This panel explores what “shifting left” means in the age of AI, from evaluating foundation models and code generation for accessibility, to embedding inclusive intent directly into design and development workflows. Panelists discuss how AI-assisted design tooling, automated scanning, and remediation in engineering pipelines can work together to prevent defects before they ship. Drawing on internal and external perspectives, the session presents a closed-loop view of accessibility that spans creation, evaluation, and remediation at AI speed.</p></blockquote><p>Joining me were four people doing important work from very different angles:</p><ul><li><a href="https://www.linkedin.com/in/dylanbarrell/">Dylan Barrell</a> of Deque Systems, who is focused on making accessibility sustainable and practical within modern engineering workflows.</li><li><a href="https://www.linkedin.com/in/momentine/">Yumeng Ma</a>, a PhD student at the University of Washington whose research examines AI, accessibility, and how code generation systems perform on real accessibility tasks.</li><li><a href="https://www.linkedin.com/in/edsummersnc/">Ed Summers</a>, Head of Accessibility at GitHub, who has been helping shape how accessibility can show up in tools, pull requests, and developer culture.</li><li><a href="https://www.linkedin.com/in/nthadani/">Navin Thadani</a>, co-founder and CEO of Evinced, whose team works on deterministic accessibility testing and remediation at scale.</li></ul><p>Our conversation centered on what “shifting left” means in the age of AI: improving model inputs, tightening prompts and instructions, adding deterministic checks, and building accessibility directly into the harnesses and processes that increasingly guide software creation.</p><p>Unfortunately, the panel wasn’t recorded, but my good friend <a href="https://www.kellygoto.com/">Kelly Goto</a> recorded the audio, so I was able to put together the following transcript. It’s a bit rough — some of the audio was garbled — but I think it captures the spirit of the conversation and the key points that were made. I hope you find it useful!</p><h2 id="full-transcript" tabindex="-1"><a class="header-anchor" href="#full-transcript" aria-hidden="true">#</a> Full Transcript</h2><p><strong>Aaron Gustafson:</strong> Welcome to our session, “Fixing Accessibility After the Fact Is Already Too Late.” Before we jump into the content, I’m going to ask everyone to introduce themselves. But because I’m cheeky, we’re going to introduce ourselves using name, role, and value. So I’ll take moderator privilege to go first. My name is Aaron Gustafson. I am a Principal TPM on the Accessibility Innovation Team here at Microsoft, and one of my values is our differences. I think we all have unique perspectives and experiences, and when we feel safe to bring our whole selves to work and to our social interactions, you really create better products and a better culture. I’m going to pass it over to Yumeng.</p><p><strong>Yumeng Ma:</strong> Yeah, hi, my name is Yumeng Ma. I’m a computer science PhD student at the University of Washington and my research is on human-computer interaction, but my interest lies in the intersection of AI and accessibility. A lot of my current work actually looks at how AI generates code and whether that code is even accessible to begin with. Something I value is making impact on people’s lives. For example, when people say AI is helping with accessibility, what drives my motivation and curiosity is really understanding how it’s helping and what ways we can improve it.</p><p><strong>Dylan Barrell:</strong> Hi, my name is Dylan Barrell. My role is cat herder at Deque Systems. I have been focusing on making accessibility sustainable. I think that’s the important thing. And in doing that, one thing that I value is how to use technology to do that. So, very excited to talk today about how we can use AI in particular and what the pitfalls and the advantages of that are, and how we can do that in a practical way. Because for us, in order for accessibility to be sustainable, it also needs to be practical.</p><p><strong>Navin Thadani:</strong> So my name is Navin Thadani. My role is the co-founder and CEO at Evinced. We’re a digital accessibility technology company. And my value, I like to think it’s a lot, but my team doesn’t let me commit code anymore.</p><p><strong>Ed Summers:</strong> I have the same problem. My name is Ed Summers. I’m head of accessibility at GitHub. And the thing that I value most is this lovely accessibility community that we have. I mean, it’s great to be with you all here. More IRL community.</p><p><strong>Aaron:</strong> Awesome. All right, so to kick us off… AI didn’t just change how we build software, it changed how defects are born and how quickly they propagate. We’re shipping code faster than ever, which is super risky, and numerous studies have shown that code generation models are not that great when it comes to generating accessible code from the jump. On top of that, the latest WebAIM Million Survey shows an uptick in accessibility violations on top home pages across the web. I’m going to direct our first question to the folks on the panel who are working to address accessibility issues at scale: What are you seeing out there? Is the uptick in the WebAIM Million an early indicator of the tsunami of accessibility issues that AI will be generating? Ed, why don’t you kick us off on that?</p><p><strong>Ed:</strong> Okay, let’s go. I don’t want to be an AI apologist or anything, but I think that the WebAIM, this year’s survey did see the increase from 51 errors or barriers found per page on average to 56. That’s kind of what we’ve been referring to here. I don’t think that is an indicator of a tsunami.</p><p>I think that a couple of things are going on. The AI models, the foundational models are trained on years and years and years of human output. So they mirror back what we do as humans. So the AI bias is human bias. And I think that’s right, and we need to work diligently to remove that bias.</p><p>But for the purposes of this discussion, can we expect a tsunami of accessibility errors due to the increases of AI? I don’t think so. I think it’s a multiply by one in the sense that we’re just going to get more of what we’ve been getting in the past as we use AI more and more. And I think if you look at the trend of the WebAIM data, for example, for the last eight years, those numbers of barriers per page, they’ve been between the range of 60 and 50. And 56 this year, 51 last year, the year before that, it was 56. So I think there’s just some noise in that data and it’s kind of roughly in line. I think what’s exciting to me is the opportunity that we have, the lever that we have with AI in order to remove bias at scale, which is obviously what we’re here to talk about.</p><p><strong>Aaron:</strong> Dylan, you had some thoughts you wanted to add on.</p><p><strong>Dylan:</strong> Yeah, I think there is, there’s been some studies that we put out. The problem with any study about AI is that AI moves so fast that by the time the study comes out, it’s sort of obsolete. It’s kind of one of those things that go on. So with that being said, one of the studies that has come out recently, for example, we’re slightly biased, of course, I understand that, but is that just defects in general are going up as we ship more code. And I think this is one of the big problems that organizations are facing today is we can generate code.</p><p>So the constraint that we used to have in our organization was often not how quickly we could create the code. And we’ve shifted that constraint from how quickly we can create code to how quickly we can determine whether the code that we’ve created is the right quality in all the different ways in order to be able to ship it. And so quality as a whole, as a problem, has become this sort of bottleneck.</p><p>People who are doing code reviews, for example, are now the bottleneck that we have to deal with. And so organizations are straining and a lot of them are putting out code and whether the bugs that they have are accessibility related, whether it’s just quality related, dealing with that problem of that new constraint is one of the biggest things that organizations have to deal with. And accessibility is a big part of that as well.</p><p><strong>Aaron:</strong> What Dylan was talking about in terms of the speed these models are changing… Yumeng, I wonder if you could talk about your research and how trying to figure out what models to map your research against was super challenging because of the models constantly changing under you.</p><p><strong>Yumeng:</strong> I think one thing we struggled the most when coming up with my research study was to select what models to evaluate. In research, a lot of what we capture is a snapshot in time, whereas models are constantly changing. Let’s say we benchmark some models now, after a few months, new models will come out. And so that’s really challenging, right?</p><p>So one way to combat this is that I have designed a testbed called PACE, which stands for Prompt Accessibility Controlled Evaluation. And essentially it’s like a playground where users have the freedom to configure how many models they select, what models they select, and alongside with what prompt they choose. And so kind of bringing that system or that testbed in, we can reuse that system to evaluate models now and even in the future.</p><p><strong>Aaron:</strong> Navin, anything you’d like to add from what you’re seeing?</p><p><strong>Navin:</strong> Yeah. So before I get to that, real quick, I did want to point out one thing. We all look at this WebAIM study every year, and we sort of kind of beat ourselves up about it a little bit. Because it doesn’t show much progress and all that. And while it’s an OK barometer at a macro level, I think that when you start kind of dissecting things, things aren’t as bad as it seems. I just want to point that out.</p><p>Our view is a lot more optimistic about where things are at, because you focus on the Fortune 500 companies, for instance, as opposed to a small online retailer in Kuwait, which is part of the WebAIM Survey. I’m not saying that’s not important, but when you look at, think of it as frequency weighting, you know, what people actually use, we find that the number of defects on a per page level are much lower, and it’s actually a good thing, and when you look at critical defects, even lower than that.</p><p>When you look at web applications as opposed to websites, people aren’t doing a good job. They’re trying. I mean, it’s far from perfect, but it’s nowhere near the doom and gloom that we all kind of think about when we look at the web. That’s the first point.</p><p>Second point is, I’m with Ed on this. The models, so far at least, are trained on the code that humans have written. And it’s mostly open source code that they’re trained on, which we all know open source is great and known for a lot of things. Accessibility unfortunately is not among them. So they do produce a lot of issues for the most part. But on a unit basis, we can expect AI to generate the same thing.</p><p>The problem is that there’s more code being written. Because it’s easy to write code now. So the total number of defects that you’re going to find could actually increase. But on a unit level, it’s probably going to be around the same. But from a, again, total perspective, it is going to increase. So that’s one inflection point that if we don’t do anything, that’s the risk.</p><p>But on the other hand, quite frankly, I think it’s easier to harness AI to actually generate accessible code much more than it is to train a developer. You look at what we’ve been doing as an industry for a long time. We’re training 100 developers; tomorrow, 33 of them are gone.</p><p>Literally, people change their job once every three years on average. And forget about the fact that it’s hard to remember what you studied. I don’t remember anything of what I studied in undergrad. Let me tell you that, for instance. So it’s very difficult to remember what you studied. And on top of that, you’re losing 33% of your employees every year.</p><p>When you look at it from an opportunity perspective, I believe we have the right opportunity and the right tools and the right motivation right now to actually reverse that trend and drive it down as close to zero as possible. So I’m very optimistic about what we can do with AI.</p><p><strong>Ed:</strong> Can I jump in?</p><p><strong>Aaron:</strong> Of course.</p><p><strong>Ed:</strong> Thank you.</p><p>Those are great points, and I think that the turnover is such a great point. It’s a challenge for us.</p><p>We talk about AI in isolation because it’s the latest tool. I think, you know, we can’t lose sight of the fact that the developer culture, you know, the culture of the relatively small number of people that build the things that the rest of the digital things that the rest of the community uses is so incredibly important. And last year, GitHub took a challenge. We took a pledge to help improve the accessibility of open source.</p><p>Part of it is what you just said about the training. And most people don’t know this, but 70 to 90% of the source code in your typical website or your typical application is open source code. If you take all the source code and put it in one big file, 70 to 90% of that source code is open source. So we have a lot of work to do there. And in improving the accessibility of open source, I think, building a culture of accessibility across the global developer community can put us in a position where accessibility becomes a portable skill that you take from one job with you to the next, as opposed to showing up for the job and like, oh, they want us to do accessibility. Okay, I’ll do it here, but I’ll do it after that. I think that’s one of our challenges here.</p><p><strong>Navin:</strong> You can always have this point using technology like MCP and all that to start going out and automatically remediating some of these issues that are out there that are common and what people use, it’s another great opportunity and definitely something that should be done. But I was reading the other day how in the Linux kernel now, there’s so many automated patches that are coming in that we just can’t keep up with that anymore. So we don’t want that same problem when we try to remediate some of the open source code. But I do think if it’s not right, that is one way to do it, we should do it.</p><p><strong>Ed:</strong> One more thing. Open source developers are amazing, and this is in no way, shape, or form. Most of us, some get paid, not that many. I don’t want to criticize them. We want you to go support open source.</p><p><strong>Aaron:</strong> But also, to Navin’s point, don’t overwhelm them with giant commits, like 10,000 file changes. Those teams cannot parse that, right? It’s not possible to go through that. So if you want to commit to open source, which I highly recommend, make sure that your commits are discrete and fixing a very specific thing and you don’t just dump a bunch of work in their lap because that’s going to be totally overwhelming.</p><p>One more thing I want to pick up on, going back to the survey, and I think that Ed and Navin both make really good points about this. We don’t talk an awful lot, or at least we don’t index really heavily on the fact that it’s home pages. And home pages, if anybody has ever worked on a home page, they are very political. They are very marketing-driven. They are not necessarily where the best choices are made. I’m just gonna throw that out there. And they’re not where people go to do tasks, right?</p><p>And so they’re not the key things that people are going to to interact with your website. They are a front door. And yes, we want that front door to be accessible. But at the same time, to your point, they’re not representative necessarily of the quality of the code that’s in the rest of the site.</p><p>All right, let’s pivot a bit…</p><p>So as everyone up here is focused on shifting left, I want to shift as far left as we can go when it comes to AI models and talk about the work we need to do in that space to improve the quality of the code that’s recommended to us. A little bit of touching on that. I think the point about this was trained on code that we all collectively wrote, which was not always code that we are proud of. I think that’s really important.</p><p>I have a friend that coined the term the “full StackOverflow developer” and I think of AI as the “full StackOverflow developer at scale” because there’s lots of stuff out there on StackOverflow that is not the best advice. And there is a real risk, to Navin’s point, as there is more AI-generated code out there, that this gets fed into the models, just like when we talk about slop content. We have slop code out there, and it becomes like the snake eating its own tail, the ouroboros, right?</p><p>So I think this is something that we need to be thinking a bunch about.</p><p>For years, we’ve been working to improve the tools and procedures that we have when it comes to identifying and remediating accessibility bugs. But that work often comes late in the software development lifecycle. When it was humans designing the UI and authoring the code, we talked about the importance of education when it comes to accessibility. And now we’re in an era where these models have nearly immediate access to far more information than humans can retain. And yet it seems that we’re struggling to get them to properly index on the right things when it comes to accessibility.</p><p>So my question is, are the issues that we’re facing in terms of code quality fundamentally about a lack of proper training data, or is it more a question of how we direct codegen models to access the data they already have? Yumeng, why don’t you kick us off on this topic? What have you seen in your research?</p><hr><p><strong>Yumeng:</strong> Yeah, I think one thing that I’ve seen in my research is that code quality isn’t just attributed to the training data and not just a prompting problem either.</p><p>For example, if we train, if we even have this ideal training data that we give to the model, the model itself is still essentially a black box. How the model, the way it interprets or thinks about the training data is also a problem. And in my research, we also looked at generating AI code with different models and with different prompting conditions. But the prompting, there wasn’t a universal prompt that made all the models reliably more accessible. There wasn’t a path where the models would output accessible code. And so it may even come down to maybe thinking of ways to even guide the AI models in a way that is interpretable for machines to learn.</p><p>Another thing that we’ve seen is that it might also be a measurement problem because a lot of the automated tools that we’re seeing. Some of the code that is generated by AI actually passes automated tools. But if you look at the semantic structure, because in my research, I’m focusing on a lot of HTML code, the semantic structure is not exactly accessible to people with disabilities.</p><p><strong>Aaron:</strong> Maybe technically valid, but not actually.</p><p><strong>Yumeng:</strong> Right.</p><p><strong>Aaron:</strong> Do you have something you want to add Dylan?</p><p><strong>Dylan:</strong> Well, yeah. I think people are talking about trust. Can we trust what the models are outputting? And I don’t think that’s even a relevant question, because we don’t trust the code that our human collaborators create either.</p><p>So for me, it’s not about trust at all. It’s about how do we take the output that’s being generated and how do we test it to make sure that it, in fact, has the quality that we want, or which one of them is accessible, right? And this comes back to the question I talked about with scaling this, because this becomes a scaling problem.</p><p>But aside from the scaling problem, there are certain approaches that we can take. And some approaches are better than others.</p><p>If we, for example, take agents and we use them to test the output of other agents, and we give them very specific prompts on what to do, they can do a decent job of improving the quality of that code overall. But one of the things that we found, and I think this is interesting because Ed, I think Ed’s gonna probably talk about this later, a study that Eric Bailey of GitHub did surfaced this as well and there’s a couple of problems that he had with that.</p><p>First of all, models in and of themselves have been trained to want to output something, right? And, in fact, the head of Anthropic product development said this. They’ve explicitly trained Claude to be a willing partner that can do and that’s action oriented, right? And so everything they’ve put into the way that they created system prompts and the way they’ve trained it makes it a great tool for us interacting with it when we’re coding. But that same attribute also makes it really bad at doing a comprehensive job of auditing anything, actually.</p><p>And you see this explicitly in code reviews. If you ask Copilot to do a code review for you, and then you ask it to do a code review again, on the same code, without having changed anything in between, you’re going to get two different perhaps overlapping sets of results back. So what you need to understand is the strengths and weaknesses of the models, what they’re good at and what they’re not good at, right?</p><p>One of the things that you find, if you try to use the agents to just audit each other, is total costs start skyrocketing. And total costs is going to become more and more of a problem. Early on, Anthropic and the other companies had been subsidizing the token costs a lot. And we’ve been using them a lot. But now you’re starting to see the pricing models change. So token cost becomes a big thing. And I think in Eric’s study that he did, that was one of the first things they ran into.</p><p>So I think what you’ve got to do is you’ve got to take a combination of more deterministic things that don’t eat up your token costs and you’ve got to combine those with the strengths of the models to really get to where you need to get to.</p><p>Looking at the speed, speed is another thing. The agents take a long time. So speed, token cost, and determinism are sort of important attributes. And that’s, I think, where some of the approaches that we’re taking at Deque can really tie together with this. Where we enhance the agents in a deterministic way with things that do comprehensive audits that bring the same results back over and over and over again. And you tie those together with the capabilities of the model is where you achieve the best results. I think that that’s sort of the approach that you have to take toward accessibility in these AI workflows.</p><p>Because for me, as Navin pointed out, that is the opportunity here. The opportunity is to teach your agentic workflows how to do accessibility. And if you can do that and do a good job of that, then we can really leverage the power of AI to not only move us forward in all the innovative areas, but to do that in a way that’s also accessible at the same time. So that, for me, is the way we should be thinking about this, using the AI where the AI is powerful and using other approaches where that’s more appropriate.</p><p><strong>Aaron:</strong> I’m going to skip over to Navin real quick and go to Ed, since Eric’s piece was mentioned a couple times. I don’t know, Ed, do you want to talk to Eric’s piece that just came out?</p><p><strong>Ed:</strong> Yeah, I’ll give you the TL;DR. So Eric Bailey is a designer at GitHub on our accessibility team, our accessibility design team. And just simply an amazing accessibility professional. He’s worked for the last few months, two or three months, building our internal accessibility agent inside of GitHub. He just published a blog post Thursday, this past Thursday, maybe it was Friday, on <a href="http://github.com/blog">github.com/blog</a>. If you look for it, just look for the accessibility tag.</p><p><strong>Aaron:</strong> It’s in the resources as well.</p><p><strong>Ed:</strong> Okay, great. It’s a treasure. I reread it again this morning and I was like, oh yeah, there’s so many things that we can learn that he was able to back into, I don’t know, like a thousand words. I want to highlight a couple of those as well.</p><p>One thing is the work that Eric did building our agent leveraged years of meticulous human effort of manually testing, manually auditing, and tracking and curating our own data set, our accessibility violations that we found within the company. I mean, literally thousands of them, most of them have been fixed, but they’re all cataloged and tagged with WCAG criteria and steps to reproduce, just really well curated data. Much of that, by the way, thanks to Dennis Lembree, who runs our governance program.</p><p>And from this knowledge, this corpus of data, oh, by the way, important thing, in addition to excellent issue descriptions, there’s a link to the PR that resolved those issues. Okay, so we have this diff. And it’s not just random, it’s our tools, our design system, the way that we work at GitHub, which is critically important to get that context, I think. And from this corpus of knowledge of data, Eric built this agent. And it’s starting to deliver really nice results. We’ve got it kind of pinned down on the lab bench, and we’re running a pilot study right now internally.</p><p>One of the ways we’re using it is on reviews of pull requests that come in for <a href="http://github.com">github.com</a>. And we’re getting really great results from that. Part of the reason they’re getting great results, I think it was about 60% resolution rate for those pull requests.</p><p>There was one more point I wanted to make about that was… oh yes, the bias towards action that Dylan mentioned. AI models, they’re going to go try and do something for you. Regardless of what you tell them, they’re going to do something. It may be what you want. It may be what you don’t want. But what Eric was able to do is he put a lot of limiting factors on this agent. So we put it in a box and tell it “Okay, these are the things you’re good at. Don’t try anything outside of that box and just don’t generate noise.”</p><p>Some of the things that are outside of that box are really complex interactions, like, for example, drag and drop, or data grids or tree grids. These are the things that are really hard to get right. And we just walled off the agent and said, “No, the humans are going to take care of that for now.” And that allows us to build confidence and trust in the PRs, the feedback that the agent’s given on PRs. I encourage you to check out the blog post if you get a chance.</p><p><strong>Aaron:</strong> Yeah, I love the idea of feeding our own bug data. I think that’s such an unused treasure trove of information. And, similar to the GitHub team’s experience, in my own tests, taking and remediating bad code coming from AI coding agents, remediating that manually and then feeding those diffs back to the agents resolved the issues 100% of the time going forward. So having that as part of the training corpus really improved things.</p><p>Navin, I’d love your thoughts on this.</p><p><strong>Navin:</strong> I’m going to echo a little bit of what Dylan said as well, because I think, and I’ll add a couple points to that, at the end of the day, the problem is quite straightforward. AI models are non-deterministic. They’re going to generate code, different kinds of code, every time you ask them. The only way to actually make it useful and ensure that things like accessibility are done is via deterministic testing. That is it. And right now, of course, it’s evident to see that if you ask AI to generate a web page, we found so many times there are so many studies out there. 47 errors, and even if you prompt it, then it becomes 46 errors, whatever.</p><p>But let’s say we’re optimistic, and eventually we get however long it takes for improvements. But even if AI is generating accessible code for the most part, you always want to validate it. You have to. So now the question is, what kind of deterministic testing do you have? How much are you covering? How accurate is it? And the kind of things that you were talking about earlier in terms of semantic structure and not being tested by. But I can tell you that those kinds of things are possible. And we’re doing those today. So the more you cover, the more you can fix automatically and ensure that what comes out is an even higher level of accessibility. So this stuff really matters.</p><p><strong>Aaron:</strong> Absolutely!</p><p>A couple of folks had mentioned the pull request, the humble pull request. It’s often the last line of defense when it comes to preventing accessibility bugs from reaching customers. Catching it directly, remediating bugs, or at least proposing remediations with a human-in-the-loop scenario is something AI agents have proven to be really good at.</p><p>How should we be thinking about these automated systems as part of the software development lifecycle loop? Is the PR the right place for these gates and checks to live. Dylan, do you want to start us off?</p><p><strong>Dylan:</strong> Well, I think the PR is too late.</p><p>As I’ve always said, we need to shift it as far left as possible. And we need to shift it all the way into design. And then during code generation, we need to be testing the code as we’re generating it before we even submit the pull request. So I think at every step along the way, you have to inject tests into that.</p><p>So, for me, the pull request is a necessary thing. We have to have the tests in the pull request because they catch things that slip through in our process, things that slip through in our technology, in our approach, and in our testing. So we need it, but it’s not enough. So for me, it’s about what are those type moves?</p><p>Everybody talks about agentic development. What most people are doing today is sitting in front of the coder and they’re asking it to do stuff and then they’re testing that, right? But where it’s moving towards is sort of fully agentic development where you have humans at particular points. In fact, that’s the only way we’re really going to scale is by elevating humans to the points where they can make the really high quality decisions. And then the coding agents do the rest.</p><p>So for me, as we see this evolution from where we are to where we’re moving towards, the question becomes “How do you test at every stage in the loops where, as Matt Pocock likes to call it, the AFK loop, right, where the human is away from keyboard and the agents are working. For me, it’s about how do we insert accessibility thinking, accessibility coding, and accessibility testing at every stage along the way, including where the human is away from the keyboard? As well as presenting that information to the human so that they can make a determination. Has this agent, has this agent’s coding loop, have they done the accessibility testing in the way that they should have done that? And we need to be looking at the results.</p><p>So for me, that’s kind of where things are moving to and those sort of approaches that we should be thinking about.</p><p><strong>Aaron:</strong> Yeah, I love that.</p><p>I’m curious, just kind of for the audience: from y’all’s perspective, there are so many tools that we have now for when we’re working with agents in the coding context. We have instruction files, we have agent files, we have sub-agents, we have orchestrators, we have skills. We have deterministic tools that we can have the agents run, like axe-core or Evinced’s unit testing tools, those sorts of things. How should people be thinking about all of these tools and how they work together and how to get the most bang for the buck? And I didn’t even mention MCP.</p><p>All of these different things… Navin… do you want to kick us off on that one?</p><p><strong>Navin:</strong> Yeah, I can describe what we see as a basic scenario and then I’ll talk about a little bit more advanced one.</p><p>At the basic level, you know, you imagine you’re a developer sitting in front of or whatever, and you just build your feature, some form on some page, drop downs, whatever. Where you can just go ahead and say, “All right, but remediate that,” or “fix that,” or whatever. And it’ll basically run a deterministic test. It’ll then come back with a set of issues. And then you can give the machine, the agent, in this case, agent-specific fix instructions. You manage the context in a certain way. And all of that can now be fixed automatically. So, as Dylan was saying, when you do your pull request, it’s already cleaned, at least for the most part, right?</p><p>That’s one way to do it, but there is a problem in that: the developer still has to do something. They have to go out and actively say, I want to fix any accessibility issues in this piece of code that I just developed before I push it further down the line. Sometimes it’s hard to do even that, to be honest. That’s just where we are.</p><p>There are many ways that you can get around that and have it have this sort of deterministic testing, these guardrails or the new word these days is “harness”. Harness engineering or whatever. But you get in many ways. There’s the skill files you mentioned, there’s configurations at the repo level, there’s all kinds of things you can do. You can integrate it into automated tests that happen as well.</p><p>So you write an automated test, the agent writes an automated test, inserts accessibility in there. If there’s an issue, a GitHub Action gets triggered, code gets fixed, and there you go.</p><p>It can happen automatically as well. So I think it really depends on your development model. It depends on the architecture that you have internally. But all the necessary ingredients are there already to be able to significantly improve the quality of code from an accessibility perspective, which is out of the box right now.</p><p><strong>Dylan:</strong> I think two words you said triggered me because I think about these a lot. And these are things that I think everybody who’s in the accessibility industry should be paying attention to. There’s two words: “harness engineering” and “context engineering,” right? And if you hear these words inside your organization, this is your opportunity because, I think Navin hinted at it if he didn’t say it, but one of the things that’s a huge opportunity from an AI coding perspective, from an accessibility perspective is AI agents do what you tell them to do. They don’t complain about it. They don’t say, no, that’s too much work, or I don’t know how to do that, whatever. They just do it. And the better the context you give them and the better the tools that you give them in order to be able to do that, the better the job that they’re going to do.</p><p>So what harness and context engineering is about is about how we engineer these tools that are becoming more and more automated. What sort of process we build, where the human gets pulled into it and when they don’t, what gets created as the artifact that comes out of that, that then has to be reviewed.</p><p>And the context is important because context is at least three things:</p><ul><li>It’s the context of the repo that you’re working on. What are the rules of that repo? What sort of tools are available in that repo? How do you do testing in that repo, et cetera?</li><li>And then there’s a context more generically about how do we do accessibility testing in this company. That’s sort of repo agnostic.</li><li>And you have to tie these things together in order to achieve the outcome.</li></ul><p>So harness engineering is not only about creating that, but it’s also about looking at the data as stuff is moving through that and analyzing what is working well. This is a much bigger topic than just accessibility, but accessibility is one of those things.</p><p>So one of the things you can do if you’re setting up your harness correctly is you can create these history files where you can look at the interactions between the different agents and what caused that churn. And churn is bad from a lot of perspectives. It costs money, it costs time. And you can then say, why are they churning? And then you can modify your context and your tools and your harness to eliminate that source of churn.</p><p>When you hear people talking about this is an opportunity for you as an accessibility practitioner to say “How do I get involved in those activities within my team or within my organization? So I can accessibility-enable those AI processes.” Because if organizations right now are redesigning the way they do software development, that’s a huge opportunity for us to get accessibility baked in.</p><p>So if you hear those keywords, jump on it and find out how you can get involved in making those harnesses and those context engineering accessibility-enabled. If there’s one takeaway that I’d like you to take away from what I’m saying up here today, it’s look for that and do that, because that’s the opportunity.</p><p><strong>Aaron:</strong> I want to pick out one thing that each of you touched on.</p><p>In terms of reading your model’s logs, I think that there’s huge value in doing that to look for those kinds of inefficiencies. With one caveat: There’s been some research that actually the models are lying about their chain of thought in some cases.</p><p>The second thing is, Navin mentioned, having AI help you by writing tests. I think that can be super useful, but at the same time, a lot of these models, they want to please. So they want to create a test that is going to be green.</p><p>So it would not surprise me, because I’ve seen it, I see agents mock all sorts of different API endpoints, I would not be surprised if one had mocked axe-core to have always return green.</p><p>I think those are some of the things that we need to also be aware of when we’re working with these models and just being sort of realistic about what their strengths and weaknesses are.</p><p>[To Yumeng] I’m wondering, from your research, because you were looking a lot at the ways that we were prompting models and a lot of that can then feed into the way that we’re writing agent files or skills, how we’re addressing the model. I wonder if you could share some of your insights from your research.</p><p><strong>Yumeng:</strong> Yeah. So what we looked at is different levels. In my research, we looked at different levels of accessibility guidance. So the lowest guidance would be no guidance. And then the second level would be just saying the keyword “accessible” in the prompt. And then the third would just be taking some factors from WCAG and turning it into the guidance and seeing how the AI responded. But I think it dives deeper than just prompting, because what we found is that there’s not a magical prompt that made the AI code output more accessible.</p><p>I think it’s not as simple as just saying, oh, just tell the AI agent to write accessible code. I think that puts a lot of pressure on the user and a lot of trust on the AI model. And so I think there’s a lot of factors beyond just prompting and just guiding the model.</p><p>So again, it feels like a big black box where we’re not certain what the factor is. It could be the evaluation, it could be the model prompting, could be even that prompting can be configured with this overall system instruction versus the specific request or task you’re asking it to do or what you’re asking it to output. Because what we’re seeing in my research, we’re looking at specifically HTML form components, is variability. Easier form components like, for example, a submit button might be just more accessible by default. As opposed to, let’s say, something more complicated like a toggle switch or something with validation or helper text, et cetera.</p><p><strong>Aaron:</strong> All right.</p><p>Ed, I wanted to come over to you, and I actually wanted to ask you a little bit to talk about the GitHub Accessibility Scanner and some of the work that you’re doing in the PR space. You made an interesting point as well in one of our early discussions about this, about the opportunity for just-in-time training for engineers who are reviewing PRs from your accessibility agent.</p><p><strong>Ed Summers:</strong> Yeah, sure. So recently, in the last few months, we released our accessibility scanner.</p><p>There’s a couple key things about that. One, it’s based on our deterministic scanning. And then we just recently added the plugin architecture, which has opened up lots of new possibilities for interactive testing.</p><p>The first built-in plugin that we’re shipping, that’s available to everybody, as opposed to something you can do on your end in your repo, tests for reflow. 1.4.10? I think it’s 1.4.10. Yeah, definitely, I can’t remember the numbers. So stay tuned for more interesting things coming out there.</p><p>Also, if you’re using the scanner, you can start to build your own custom extensions there. So by default, the scanner will assign issues that it creates, which are the descriptions that we see are good for prompting, we’ve been working a lot on prompting. The description of the issues that are filed are essentially prompts for GitHub Copilot to create a PR for the fixes. And those prompts, like Yumeng is saying, they really do matter.</p><p>Then the other thing that’s really relevant here, and that everybody can do, this is almost a freebie, you know, there are certain things we can do that are almost, they’re really easy to do, and they’re gonna have a pretty large positive impact with relatively few negative impacts, is to use custom instructions, which we also might call the system prompts.</p><p>The place to go look for the latest thinking on custom instructions is Michael Fairchild. He works in Microsoft’s security division. He’s done really great work on evaluating how custom instructions affect outputs with different models. Do you have that in our resources?</p><p><strong>Aaron:</strong> Yep, it’s there.</p><p><strong>Ed:</strong> It’s called the A11y LLM Eval. And if you combine this kind of automatic assignment of issues to Copilot with custom instructions in your repo, with good model selection, because we know that model selection matters, you can just really, you can start to eliminate accessibility debt before it gets started, which is really what we want to do.</p><p><strong>Dylan:</strong> Yeah, we found the same thing, actually.</p><p>We have, as part of our MCP tool, we have a remediation tool where you pass it the context of the defect. And it uses the Deque University knowledge in the back end to then give you back the exact context you need on how to address that problem. And we found that that, together with the fact that the agent itself has the context of the source code, allows it to then really translate that very specific guidance on that particular type of issue into a really high quality remediation very, very, very consistently.</p><p>So yeah, there’s ways you can do that in a sort of a static way and then there’s ways you can do it sort of more dynamically like that as well.</p><p><strong>Navin:</strong> Yeah, there’s one point I’d like to make.</p><p>We’re talking about these sorts of things and in some ways there’s a bigger risk that we haven’t addressed. We’re kind of sitting here, for the most part, all of us care a lot about accessibility. We’re also involved with our development teams, and we think about it a lot. But unfortunately, this is a bit of an echo chamber, because we’re not considering what the rank and file front-end developer has to go through and is thinking about on a day-to-day basis. And it’s not very much accessibility, unfortunately.</p><p>So I can give you an example, and it’s best to understand this through an example. I was talking to an engineer, front-end engineer, at a healthcare company. And we kind of challenged her and turned around and said, “All right, but how are you going to solve accessibility? Forget the tooling and all that. What’s your basic problem here?” So she goes to Claude Code and says, “Test for accessibility.” And it came back with some number of issues.</p><p>Then she said, “OK, go fix these issues.” So no validation of whether the issues are correct, not this or that. Go ahead, fix these issues, and it turns out that the 30 issues that Claude tried to fix all at once just inundated the context entirely. There were no guardrails, there was nothing. And talk about thrashing, this was token burning. This is what’s going on. I mean, people think like that.</p><p>So, yeah, we can all sit here and say, we’ve got tools, we’ve got harnesses, and we can remediate things automatically, this and that. But at the end of the day, front-end architects in these large development teams need to be the ones to actually integrate these sorts of technologies into the fabric. Because otherwise it’s not happening.</p><p><strong>Aaron:</strong> Yeah, put some rigor around that process.</p><p><strong>Navin:</strong> Yeah, I mean, not everybody’s like Ed. Not every company is like Microsoft, where you can try to build your own tools and this and that. Certainly, you can try. And if you’re an average enterprise out there, do you want to be in the business of building accessibility tooling for yourself?</p><p><strong>Ed:</strong> This comes back to one of your questions you had earlier that we forgot to mention, the kind of the opportunities around PRs, and including AI and PRs. And that’s a just-in-time training opportunity for those developers. Because that developer can go off and say, “OK, test accessibility,” then fix all those. And then she’s going to create a PR, but if all that’s trash, then we can put a system in place to catch that slop at the PR. And, in doing so, in order to address the issue comments or PR comments that are being raised by a good accessibility agent, that required the human developer to read those and accept or reject them. In reading those, and I see this all the time in the work that we do at GitHub, the explanations of why the diff is being suggested in PR are really good.</p><p>In one of our discussions when we were preparing for this panel, we discussed taking the outputs and pulling them back around. If we can go back to the humans and train them along the way, especially at scale, because every PR and every code change that you ship in your organization has to go through a PR process and you’re getting feedback to your developers and just in time training literally hundreds of thousands of those a day, that’s going to make a big difference.</p><p><strong>Dylan:</strong> Actually, your organization put out, together with The Pragmatic Engineer, a study. I read it yesterday. So, interesting thing when you mentioned developers reading comments… I don’t think they read them anymore. And I think one of the things that came out of that study was leadership and organizations pushing more and more for more and more speed, and they don’t care about the code quality as much anymore.</p><p>So there’s a lot of pressure on developers to just sort of say, fix it, like Navin said. That’s the response. “Oh, you found these things? OK, fix them” becomes the challenge, becomes the standard response.</p><p>So I think that’s a reality that we have to accept. We’re moving into a situation where developers, they’re no longer writing code. They don’t write code, nobody writes code. But they’re going to stop reading code pretty soon too. And they’re not going to read any lines of code ever. So I think it’s great to talk about kind of where the past was, but I think what we should be thinking about is where it’s moving too, because we need to set ourselves up for that future where developers aren’t reading code, developers aren’t writing code. And so now what do we need?</p><p><strong>Aaron:</strong> It becomes really important that we teach people how to ask the right questions for the model and ask, “Oh, why did you approach it this way?” Or, before they start coding, say “Develop a plan for me to explain why you need to make all of the changes that you’re suggesting and what they’re going to fix.”</p><p>Circling back to the tons of issues that get found by AI, I’d say the same thing goes for accessibility testers. If your accessibility testers aren’t particularly knowledgeable, you can end up with a lot of noise in that space as well, where you’ve got things that people are suggesting, fixes that are actually going to make the system less accessible.</p><p>We’ve got about 10 minutes left, so I’m going to try and steer us towards the end here.</p><p>No single org can solve this alone. The ecosystem has to interoperate and we have to learn from each other if we’re going to crack that nut, which is why I have all of you on stage. So, to meet this moment, we need to get all hands on deck. We need to improve the models themselves. We need to consider how we prompt them. We need to consider the tools that we hand them, how they integrate into our current and future processes.</p><p>We also need to recognize that AI’s ability to help us scale up our ability to find remediate bugs is not the end of the story, it’s only the beginning. If anything, it frees us up to shift the focus from simple accessibility bugs that we just can’t seem to eradicate to more complex interactions that we need time to ponder and to explore, places that we need to apply our human creativity and our human ingenuity.</p><p>So as we close out, I’d like to pass the mic around one more time to our panel and ask everyone to please share something that excites you about this moment or something that you would like to encourage our audience to investigate further.</p><p>Navin, do you want to kick us off?</p><p><strong>Navin:</strong> Yeah, I think there’s only one thing, and I’ll say it again. It’s deterministic testing. That is the most important thing in today’s day and age.</p><p><strong>Ed:</strong> I’ll make it quick too. I’ve come up with a little metaphor for a strong accessibility leader in today’s rapidly changing environment. Because, you know, we talked so much about AI, but the people, and this whole thing are so important. The accessibility professionals out there who are dealing with this rapid change that’s being forced upon them, you know, and are rapidly adopted, might be overwhelming.</p><p>Most accessibility programs are one person. So I was thinking, you know, what does a strong accessibility leader look like today? My metaphor for that is a dumbbell, you know. And on one end of the dumbbell, we have an eagerness, an embracing of this opportunity that we have with AI, knowing that it’s changing everything, knowing all the uncertainties and how rapidly it’s changing. But the only way to deal with that, it’s one thing that going blind teaches you: When change comes, you just kind of get in there and embrace it. You cannot fight this. So I think leaders need to embrace this change and jump into these discussions, with these panelists here and the other folks that were mentioned.</p><p>And then the other end of the dumbbell is a focus on the humans. That’s the humans that use our technologies that need accessibility with a focus on user research. User testing with our friends from Fable or other organizations like that is absolutely critical. Design, actual thoughtful design. And also a good understanding of the human psychology of the developers, the people that are in the pressures upon them, both in the open source community and in the private sector. Because the psychology of those developers, those designers, those professionals is a factor that can make or break our success when we’re putting together, when we’re building accessibility programs.</p><p>And then the bar between those two ends of the dumbbell is that strong leader that’s embracing the human side and the rapidly changing AI side.</p><p><strong>Aaron:</strong> Okay, Yumeng.</p><p><strong>Yumeng Ma:</strong> Yeah, what a great metaphor. I’m going to say that I would encourage everyone to look at the gap between accessibility checkers or checks and support accessible use. That comes down to evaluation design. Because a lot of AI right now is, essentially, all the benchmarks that I’ve been seeing is always so automated. And a lot of these automated checks might say something is accessible. For example, alt text is a great way to think about it, where the automated checker might look at alt text and be like, oh, well, yeah, this image has alt text, so it passes on automated checker. But if alt text isn’t meaningful, then what’s the point of having alt text to begin with? And so yeah, just seeing what are we even evaluating. What’s missing? What are we really incentivizing AI to look at? And because if our metrics are shallow, then the models that it’s trained on might just learn accessibility that’s shallow as well.</p><p><strong>Aaron:</strong> Dylan, would you like to take us home?</p><p><strong>Dylan Barrell:</strong> Yeah, I think you probably could guess what I’m going to focus on.</p><p>You know, Ed said something very important, I think, which we need to take into account when thinking about what to recommend to people: If there’s one accessibility person in the organization, what do you do in a world where AI is becoming more and more the way we do things? AI is speeding everything up. It’s shifting constraints. We’re struggling as organizations on how to deal with those constraints. What can a single person do or a small number of people do?</p><p>They can find the leverage, the high leverage points and try to insert themselves there.</p><p>So when I talked about context engineering, harness engineering, those are the points that they can look to insert themselves in to get the most leverage, right? Don’t spend your time evaluating alt text of images. Don’t spend your time doing manual audits or whatever. Try to spend your time figuring out how to make that process, that AI-driven process, accessible so that you can elevate yourself into the position where you can do things that are more valuable. Because the stuff at the end or the beginning in terms of user research is where accessibility experts should be spending their time. But if they’re constantly spending their time evaluating alt text and doing accessibility audits manually, then they never get the time to do that.</p><p>And AI, for me, AI leveraged in the right way is a huge opportunity for them to get out of the job of dealing with that really grunt work and doing the stuff that really matters. And there’s two things that matter. It’s the stuff at the front and the back end, the discussion and the user research. And it’s making the process produce accessible stuff by default.</p><p><strong>Aaron:</strong> Awesome. What a great note to end on.</p><p>I want to thank our panel very much for their insights today, for sharing their time with us.</p><p>And to the audience, I want to thank all of you for having us. If you don’t mind taking a moment and sharing your feedback on the session, we would absolutely appreciate that as well.</p><h2 id="thank-you" tabindex="-1"><a class="header-anchor" href="#thank-you" aria-hidden="true">#</a> Thank You</h2><p>My thanks again to Yumeng Ma, Dylan Barrell, Navin Thadani, and Ed Summers for a thoughtful conversation and for sharing so much practical insight from their respective corners of the accessibility and AI ecosystem.</p><h2 id="resources-shared-during-the-panel" tabindex="-1"><a class="header-anchor" href="#resources-shared-during-the-panel" aria-hidden="true">#</a> Resources Shared During the Panel</h2><ul><li><a href="https://github.com/github/accessibility-scanner">GitHub Accessibility Scanner</a></li><li><a href="https://github.blog/ai-and-ml/github-copilot/building-a-general-purpose-accessibility-agent-and-what-we-learned-in-the-process/">Building a general-purpose accessibility agent-and what we learned in the process</a></li><li><a href="https://aimac.ai/">AIMAC: The AI Model Accessibility Checker</a></li><li><a href="https://github.com/momentine/pace">Prompt Accessibility Controlled Evaluation (PACE)</a></li><li><a href="https://microsoft.github.io/a11y-llm-eval-report/">A11y LLM Eval</a></li></ul>]]></content><amg:twitter><![CDATA[At Ability Summit 2026, I moderated a panel on shifting accessibility left in the age of AI with Yumeng Ma, Dylan Barrell, Navin Thadani, and Ed Summers.]]></amg:twitter><amg:summary><![CDATA[<p>At Ability Summit 2026, I had the pleasure of moderating a panel on a topic that feels more urgent by the day: how we prevent accessibility defects before they are baked into AI-assisted workflows and shipped at scale.</p><blockquote><p>As AI accelerates how software and content are created, accessibility risks emerge earlier and propagate faster than traditional testing and remediation can handle. This panel explores what “shifting left” means in the age of AI, from evaluating foundation models and code generation for accessibility, to embedding inclusive intent directly into design and development workflows. Panelists discuss how AI-assisted design tooling, automated scanning, and remediation in engineering pipelines can work together to prevent defects before they ship. Drawing on internal and external perspectives, the session presents a closed-loop view of accessibility that spans creation, evaluation, and remediation at AI speed.</p></blockquote>]]></amg:summary><summary type="html"><![CDATA[<p>At Ability Summit 2026, I had the pleasure of moderating a panel on a topic that feels more urgent by the day: how we prevent accessibility defects before they are baked into AI-assisted workflows and shipped at scale.</p><blockquote><p>As AI accelerates how software and content are created, accessibility risks emerge earlier and propagate faster than traditional testing and remediation can handle. This panel explores what “shifting left” means in the age of AI, from evaluating foundation models and code generation for accessibility, to embedding inclusive intent directly into design and development workflows. Panelists discuss how AI-assisted design tooling, automated scanning, and remediation in engineering pipelines can work together to prevent defects before they ship. Drawing on internal and external perspectives, the session presents a closed-loop view of accessibility that spans creation, evaluation, and remediation at AI speed.</p></blockquote>]]></summary><category term="accessibility" /><category term="AI/ML" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/matching-form-fields-with-a-web-component/</id><title type="html"><![CDATA[Easy Data-entry Verification with a Web Component]]></title><link href="https://www.aaron-gustafson.com/notebook/matching-form-fields-with-a-web-component/" rel="alternate" type="text/html" /><published>2026-05-25T22:23:24Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Sometimes the simplest form pattern ends up being the one you repeat <i lang="la">ad infinitum</i>: “Type it once, then type it again.” We do it for password confirmations, email verification, and any flow where a typo can create expensive follow-up work. This week I released a small web component to handle that pattern cleanly: <a href="https://github.com/aarongustafson/form-matching-fields"><code>form-matching-fields</code></a>.</p><p>The component wraps two related form fields and adds validation to ensure they match. It’s intentionally additive, which means it works with native browser validation rather than trying to replace it.</p><h2 id="wrap-and-go" tabindex="-1"><a class="header-anchor" href="#wrap-and-go" aria-hidden="true">#</a> Wrap and go</h2><p>The API is intentionally simple:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-matching-fields</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Password<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password-again<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Password again<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password-again<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-matching-fields</span><span class="token punctuation">&gt;</span></span></code></pre><p>The component evaluates the first two eligible text-type inputs it contains and applies mismatch validation to the second one when both fields have values.</p><h2 id="what-counts-as-an-eligible-field%3F" tabindex="-1"><a class="header-anchor" href="#what-counts-as-an-eligible-field%3F" aria-hidden="true">#</a> What counts as an eligible field?</h2><p>To keep behavior predictable, the component only considers descendant <code>input</code> elements of these types:</p><ul><li>text</li><li>email</li><li>password</li><li>search</li><li>tel</li><li>url</li></ul><p>It ignores <code>disabled</code> and <code>readonly</code> controls, which helps prevent false positives when you have conditional or locked fields in the same wrapper.</p><h2 id="validation-behavior-that-plays-nicely" tabindex="-1"><a class="header-anchor" href="#validation-behavior-that-plays-nicely" aria-hidden="true">#</a> Validation behavior that plays nicely</h2><p>One of the core goals here was to avoid stepping on existing validation rules. In practice, that means:</p><ul><li>If the second field already has a native validation issue (like <code>required</code> or <code>type</code> mismatch), this component won’t obscure that.</li><li>If the second field already has a custom validity message, this component won’t overwrite that either.</li><li>It only clears mismatch errors that it set itself.</li></ul><p>That last point is especially useful in larger forms where multiple constraints can overlap. You don’t want one helper trying to be the sole source of truth.</p><h2 id="customizing-the-validation-message" tabindex="-1"><a class="header-anchor" href="#customizing-the-validation-message" aria-hidden="true">#</a> Customizing the validation message</h2><p>By default, the component uses this template for the validation message:</p><blockquote><p>The fields “{label_1}” and “{label_2}” should match</p></blockquote><p>It smartly resolves the labels (<code>{label_1}</code> and <code>{label_2}</code>) from your markup in this order:</p><ol><li>associated <code>&lt;label for&gt;</code> text</li><li>wrapping <code>&lt;label&gt;</code> text</li><li><code>aria-label</code></li><li><code>name</code></li><li><code>id</code></li></ol><p>You can override it with the <code>validation-message</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-matching-fields</span><span class="token attr-name">validation-message</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Please ensure {label_2} matches {label_1}.<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>verify-email<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Verify email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>verify-email<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-matching-fields</span><span class="token punctuation">&gt;</span></span></code></pre><p>In most forms, that gives you a clear error message without requiring extra setup.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I put together <a href="https://aarongustafson.github.io/form-matching-fields/demo/">a demo of the web component</a> over on GitHub:</p><figure class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-matching-fields/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>You can explore the source, file issues, and suggest enhancements in <a href="https://github.com/aarongustafson/form-matching-fields">the <code>form-matching-fields</code> repository</a>.</p>]]></content><amg:twitter><![CDATA[Need two form fields to match? `form-matching-fields` adds simple validation for confirmation fields.]]></amg:twitter><amg:summary><![CDATA[<p>Sometimes the simplest form pattern ends up being the one you repeat <i lang="la">ad infinitum</i>: “Type it once, then type it again.” We do it for password confirmations, email verification, and any flow where a typo can create expensive follow-up work. This week I released a small web component to handle that pattern cleanly: <a href="https://github.com/aarongustafson/form-matching-fields"><code>form-matching-fields</code></a>.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Sometimes the simplest form pattern ends up being the one you repeat <i lang="la">ad infinitum</i>: “Type it once, then type it again.” We do it for password confirmations, email verification, and any flow where a typo can create expensive follow-up work. This week I released a small web component to handle that pattern cleanly: <a href="https://github.com/aarongustafson/form-matching-fields"><code>form-matching-fields</code></a>.</p>]]></summary><category term="accessibility" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/visual-validation-feedback-for-form-fields/</id><title type="html"><![CDATA[Visual Validation Feedback for Form Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/visual-validation-feedback-for-form-fields/" rel="alternate" type="text/html" /><published>2026-04-22T20:17:47Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Password requirements, username rules, input format constraints: forms often have multiple validation requirements, but users frequently do not find out whether they are meeting them until they hit submit. The <code>form-validation-list</code> web component changes that by providing real-time visual feedback as users type, showing exactly which requirements are met and which are not.</p><p><ins datetime="2026-04-30T00:00:00+00:00"><strong>Update:</strong> This post has been refreshed to cover the component’s current loading options, throttled input behavior, accessibility model, and localization hooks.</ins></p><p>This is a modern replacement for my old <a href="https://github.com/easy-designs/jquery.easy-validation-rules.js">jQuery Easy Validation Rules</a> plugin, reimagined as a web component with native form validation integration.</p><hr><p>To get started, associate the component with an <code>input</code> element using the <code>for</code> attribute and define your validation rules:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Username:<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>username<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[A-Z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one capital letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[a-z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one lowercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[\d]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Submit<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>By default, validation runs on the <code>input</code> event with a 250ms throttle. Matched rules get a checkmark (✓), unmatched rules get an X (✗), and while someone is typing the component announces a concise progress summary instead of repeatedly re-reading the whole rule list. When all rules match, the field is valid and the form can be submitted.</p><h2 id="what%E2%80%99s-happening-under-the-hood%3F" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-happening-under-the-hood%3F" aria-hidden="true">#</a> What’s happening under the hood?</h2><p>The component:</p><ol><li>Associates with an input via the <code>for</code> attribute (just like a <code>label</code> element)</li><li>Finds all elements with <code>data-pattern</code> attributes</li><li>Tests the input value against each pattern when the configured trigger fires</li><li>Adds <code>validation-matched</code> or <code>validation-unmatched</code> classes and visual indicators accordingly</li><li>Inserts localized, visually hidden state text once the field has a value</li><li>Updates a single polite live region while users type</li><li>Uses <code>setCustomValidity()</code> to integrate with native form validation</li><li>Prevents form submission until all rules match</li></ol><p>The cascade animation, controlled by <code>each-delay</code>, creates a pleasant visual effect as rules are checked sequentially. It is a small touch, but a nice one.</p><h2 id="whose-rules%3F-your-rules." tabindex="-1"><a class="header-anchor" href="#whose-rules%3F-your-rules." aria-hidden="true">#</a> Whose rules? Your rules.</h2><p>Define rules using regular expression patterns in the <code>data-pattern</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Length requirements --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>.{8,}<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least 8 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>.{8,32}<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Between 8 and 32 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Character type requirements --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[A-Z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one uppercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[a-z]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one lowercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[\d]+<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>At least one number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>[!@#$%^&amp;<em>]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one special character<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- Format requirements --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.+@.+..+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Valid email format<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span><sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup>+$<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Only letters and numbers<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>Each pattern is a standard JavaScript regular expression. The component tests the <code>input</code> value against all patterns on the configured trigger, using throttled <code>input</code> events by default.</p><h2 id="input-event-too-noisy%3F-no-worries." tabindex="-1"><a class="header-anchor" href="#input-event-too-noisy%3F-no-worries." aria-hidden="true">#</a> Input event too noisy? No worries.</h2><p>By default, validation runs on the <code>input</code> event with a 250ms throttle. If you want immediate feedback while typing, set <code>input-throttle=&quot;0&quot;</code>. If you’d rather wait until the field loses focus, switch the <code>trigger-event</code> to <code>&quot;blur&quot;</code>:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token attr-name">trigger-event</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>blur<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.+@.+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Contains @ symbol<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.+@.+..+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Valid email format<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>With this attribute in place, validation runs immediately when the field loses focus. In this mode, <code>input-throttle</code> is ignored and the component keeps the full criteria list available to assistive technology while someone types.</p><h2 id="wanna-adjust-the-cascade-delay%3F-go-for-it." tabindex="-1"><a class="header-anchor" href="#wanna-adjust-the-cascade-delay%3F-go-for-it." aria-hidden="true">#</a> Wanna adjust the cascade delay? Go for it.</h2><p>Use the <code>each-delay</code> attribute to control the delay between checking each rule. The default speed is 150ms, but you can tune it to any number of milliseconds:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">each-delay</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>100<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- rules --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>Set it to “0” to remove the cascade effect entirely and check all rules simultaneously.</p><h2 id="need-full-design-control%3F-you-got-it." tabindex="-1"><a class="header-anchor" href="#need-full-design-control%3F-you-got-it." aria-hidden="true">#</a> Need full design control? You got it.</h2><p>If you want full design control over the component, you can absolutely have it. The whole component operates in light DOM, so your styles will pierce through. And you can customize <code>class</code> names for integration with CSS frameworks using a set of attributes on the <code>form-validation-list</code> element. The <code>field-valid-class</code> and <code>field-invalid-class</code> attributes control the class names applied to the <code>input</code> field itself, while the <code>rule-matched-class</code> and <code>rule-unmatched-class</code> attributes control the <code>class</code> names applied to each rule item.</p><p>Here’s a complete example:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">&gt;</span></span><span class="token style"><span class="token language-css"><span class="token selector">.is-valid</span><span class="token punctuation">{</span><span class="token property">border-color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token selector">.is-invalid</span><span class="token punctuation">{</span><span class="token property">border-color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token selector">.rule-pass</span><span class="token punctuation">{</span><span class="token property">color</span><span class="token punctuation">:</span> green<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token selector">.rule-fail</span><span class="token punctuation">{</span><span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><span class="token punctuation">}</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>username<span class="token punctuation">“</span></span><span class="token attr-name">field-valid-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>is-valid<span class="token punctuation">“</span></span><span class="token attr-name">field-invalid-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>is-invalid<span class="token punctuation">“</span></span><span class="token attr-name">rule-matched-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>rule-pass<span class="token punctuation">“</span></span><span class="token attr-name">rule-unmatched-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>rule-fail<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.{5,}<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least 5 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[!@#]+<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span>Special char (!@#)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><p>This approach lets you use <code>class</code> names that match your existing CSS architecture, rather than making one small component dictate terms to the rest of your styles.</p><p>You can also override the per-instance icon glyphs with the <code>rule-matched-icon</code> and <code>rule-unmatched-icon</code> attributes, or control the shared visual styling using CSS custom properties:</p><ul><li><code>–rule-matched-icon</code> - Content for matched state (default: “✓”)</li><li><code>–rule-unmatched-icon</code> - Content for unmatched state (default: “✗”)</li><li><code>–rule-icon-size</code> - Size of icons (default: 1em)</li><li><code>–rule-matched-color</code> - Color for matched rules (default: green)</li><li><code>–rule-unmatched-color</code> - Color for unmatched rules (default: red)</li></ul><p>The older <code>–validation-</em></code> custom property names are still supported as legacy aliases.</p><p>Here’s an example of that:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token selector">form-validation-list</span><span class="token punctuation">{</span><span class="token property">–rule-matched-icon</span><span class="token punctuation">:</span><span class="token string">”✅“</span><span class="token punctuation">;</span><span class="token property">–rule-unmatched-icon</span><span class="token punctuation">:</span><span class="token string">”❌“</span><span class="token punctuation">;</span><span class="token property">–rule-icon-size</span><span class="token punctuation">:</span> 1.2em<span class="token punctuation">;</span><span class="token property">–rule-matched-color</span><span class="token punctuation">:</span> #28a745<span class="token punctuation">;</span><span class="token property">–rule-unmatched-color</span><span class="token punctuation">:</span> #dc3545<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><h2 id="typescript-or-framework-project%3F-you%E2%80%99re-covered." tabindex="-1"><a class="header-anchor" href="#typescript-or-framework-project%3F-you%E2%80%99re-covered." aria-hidden="true">#</a> TypeScript or framework project? You’re covered.</h2><p>The package now ships with bundled type definitions and reflects its core properties and attributes in both directions. That makes it a much better fit for TypeScript, JSX, SSR, and declarative framework setups where properties may be assigned before the custom element upgrades.</p><h2 id="bit-of-a-control-freak%3F-there%E2%80%99s-an-api." tabindex="-1"><a class="header-anchor" href="#bit-of-a-control-freak%3F-there%E2%80%99s-an-api." aria-hidden="true">#</a> Bit of a control freak? There’s an API.</h2><p>If you really want to get into the weeds, you can also listen for validation changes in your JavaScript code:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> validationList <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-validation-list”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>validationList<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-validation-list:validated”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">const</span><span class="token punctuation">{</span> isValid<span class="token punctuation">,</span> matchedRules<span class="token punctuation">,</span> totalRules<span class="token punctuation">,</span> field <span class="token punctuation">}</span><span class="token operator">=</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Matched &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;matchedRules&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; of &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;totalRules&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; rules&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Field is &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;isValid &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;valid&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;invalid&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>The event fires after validation completes and gives you the current state. Nice and tidy.</p><p>You can also manually trigger validation and check the element’s current state at any time:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> validationList <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-validation-list”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Trigger validation</span><span class="token keyword">const</span> isValid <span class="token operator">=</span> validationList<span class="token punctuation">.</span><span class="token function">validate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Is valid:”</span><span class="token punctuation">,</span> isValid<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Check current state</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Current state:”</span><span class="token punctuation">,</span> validationList<span class="token punctuation">.</span>isValid<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="global-site%3F-relaje!" tabindex="-1"><a class="header-anchor" href="#global-site%3F-relaje!" aria-hidden="true">#</a> Global site? <i lang="es">Relaje!</i></h2><p>If you need the component to work in different languages, that’s totally doable. You can customize three separate pieces of copy: the browser validation message (<code>validation-message</code>), the live summary announced while typing (<code>announcement</code>), and the per-rule hidden status text (<code>rule-matched-alt</code> and <code>rule-unmatched-alt</code>). All of the message templates support the <code>{matched}</code> and <code>{total}</code> placeholders:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token comment">&lt;!-- Spanish --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>contrasena<span class="token punctuation">“</span></span><span class="token attr-name">announcement</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>{matched} de {total} criterios cumplidos<span class="token punctuation">“</span></span><span class="token attr-name">rule-matched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Criterio cumplido<span class="token punctuation">“</span></span><span class="token attr-name">rule-unmatched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Criterio pendiente<span class="token punctuation">“</span></span><span class="token attr-name">validation-message</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Por favor, cumple todos los requisitos ({matched} de {total})<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[A-Z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Al menos una letra mayúscula<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[a-z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Al menos una letra minúscula<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[\d]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Al menos un número<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span><span class="token comment">&lt;!-- French --&gt;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>mot-de-passe<span class="token punctuation">“</span></span><span class="token attr-name">announcement</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>{matched} critères satisfaits sur {total}<span class="token punctuation">“</span></span><span class="token attr-name">rule-matched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Critère satisfait<span class="token punctuation">“</span></span><span class="token attr-name">rule-unmatched-alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Critère non satisfait<span class="token punctuation">“</span></span><span class="token attr-name">validation-message</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>Veuillez satisfaire à toutes les exigences ({matched} sur {total})<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[A-Z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Au moins une lettre majuscule<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[a-z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Au moins une lettre minuscule<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[\d]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Au moins un chiffre<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span></code></pre><h2 id="is-it-a-progressive-enhancement%3F-heck-yeah!" tabindex="-1"><a class="header-anchor" href="#is-it-a-progressive-enhancement%3F-heck-yeah!" aria-hidden="true">#</a> Is it a progressive enhancement? Heck yeah!</h2><p>The component uses light DOM, so if JavaScript fails, users still see the validation requirements as a standard list. They can read what is expected even without the visual feedback. Your server-side validation still does the important enforcement work regardless… right? <em>Right?</em></p><h2 id="is-it-screen-reader-accessible%3F-yep." tabindex="-1"><a class="header-anchor" href="#is-it-screen-reader-accessible%3F-yep." aria-hidden="true">#</a> Is it screen reader accessible? Yep.</h2><p>The component is built with accessibility in mind:</p><ul><li><strong>Proper description support</strong>: The validation list is automatically associated with the <code>input</code> via <code>aria-describedby</code>, and if the field already has <code>aria-describedby</code>, the original value is preserved.</li><li><strong>A concise announcement model</strong>: With the default <code>trigger-event=&quot;input&quot;</code>, the component temporarily suspends the full criteria list from <code>aria-describedby</code> while someone types and uses a single polite live region to announce progress instead.</li><li><strong>State restoration on blur</strong>: When focus leaves the field, any pending validation timeouts are cleared and the full criteria list is restored so returning to the field announces the final criteria state.</li><li><strong>Localized rule state</strong>: Once the field has a value, each rule gets visually hidden localized state text in the DOM, which is more robust than relying on CSS-generated content alone.</li></ul><p>If you have suggestions for other ways to improve the accessibility of this component, please <a href="https://github.com/aarongustafson/form-validation-list/issues">open an issue on GitHub</a>.</p><h2 id="does-it-integrate-with-the-browser%E2%80%99s-validation-engine%3F-naturally." tabindex="-1"><a class="header-anchor" href="#does-it-integrate-with-the-browser%E2%80%99s-validation-engine%3F-naturally." aria-hidden="true">#</a> Does it integrate with the browser’s validation engine? Naturally.</h2><p>The component uses <code>setCustomValidity()</code> to participate in native form validation:</p><ul><li>When all rules match, custom validity is cleared</li><li>When rules don’t match, a custom validity message is set</li><li>Form submission is prevented until all rules pass</li><li>Works with <code>:valid</code> and <code>:invalid</code> CSS pseudo-classes</li><li>Compatible with the Constraint Validation API</li></ul><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> form <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> field <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">“username”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>form<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“submit”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>form<span class="token punctuation">.</span><span class="token function">checkValidity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Validation failed:”</span><span class="token punctuation">,</span> field<span class="token punctuation">.</span>validationMessage<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="here%E2%80%99s-a-real-world-example" tabindex="-1"><a class="header-anchor" href="#here%E2%80%99s-a-real-world-example" aria-hidden="true">#</a> Here’s a real-world example</h2><p>Here’s a complete password validation setup:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Password:<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-validation-list</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>password<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>.{8,}<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least 8 characters<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[A-Z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one uppercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[a-z]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one lowercase letter<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[\d]+<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>At least one number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">data-pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>[!@#$%^&amp;<em>]+<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span>At least one special character (!@#$%^&amp;</em>)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-validation-list</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Submit<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>Users see exactly which requirements they have met and which they still need to satisfy. That tends to be a lot kinder than springing the whole list on them after submit.</p><h2 id="play-with-it" tabindex="-1"><a class="header-anchor" href="#play-with-it" aria-hidden="true">#</a> Play with it</h2><p>Check out <a href="https://aarongustafson.github.io/form-validation-list/demo/">the demo</a> with various examples:</p><figure id="fig-2025-12-06-09" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-validation-list/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>View the project on <a href="https://github.com/aarongustafson/form-validation-list">GitHub</a>.</p><p>Install via <code>npm</code>:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-validation-list</code></pre><p>For most projects, import the guarded auto-definition helper:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/form-validation-list/define.js”</span><span class="token punctuation">;</span></code></pre><p>If you want to control the tag name yourself, import <code>FormValidationListElement</code> and register it manually.</p><p>Happy validating!</p><hr class="footnotes-sep"><section class="footnotes"><h4 class="hidden">Footnotes</h4><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>a-zA-Z0-9 <a href="#fnref1" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content><amg:twitter><![CDATA[New #WebComponent: Show users which validation requirements they’ve met — as they type.]]></amg:twitter><amg:summary><![CDATA[<p>Password requirements, username rules, input format constraints: forms often have multiple validation requirements, but users frequently do not find out whether they are meeting them until they hit submit. The <code>form-validation-list</code> web component changes that by providing real-time visual feedback as users type, showing exactly which requirements are met and which are not.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Password requirements, username rules, input format constraints: forms often have multiple validation requirements, but users frequently do not find out whether they are meeting them until they hit submit. The <code>form-validation-list</code> web component changes that by providing real-time visual feedback as users type, showing exactly which requirements are met and which are not.</p>]]></summary><category term="web components" /><category term="progressive enhancement" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="web forms" /><category term="user experience" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/never-lose-form-progress-again/</id><title type="html"><![CDATA[Never Lose Form Progress Again]]></title><link href="https://www.aaron-gustafson.com/notebook/never-lose-form-progress-again/" rel="alternate" type="text/html" /><published>2026-04-20T23:59:08Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Few things are more annoying than losing your progress halfway through a form. Maybe the browser crashes. Maybe the tab gets closed. Maybe your kid yells from the other room and you come back three hours later wondering why you ever thought now was a good time to fill out a mortgage application. Whatever the cause, <code>form-saver</code> makes those interruptions a lot less obnoxious. Which is nice, because forms are usually annoying enough on their own.</p><p>At its core, <code>form-saver</code> is a small web component that wraps a form, keeps an eye on it, stores values in <code>localStorage</code>, and restores them when the page loads again. Better yet, it clears out saved data after a successful submission so you’re not accidentally resurrecting stale information the next time someone stops by. Nobody wants yesterday’s half-finished support request shambling back to life.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>All you need to do is wrap your form in the component:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Message<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>textarea</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>message<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>textarea</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Send<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>That’s it. The component targets the first descendant <code>form</code>, saves values as users type or make changes, and restores them when they come back. No extra plumbing. Just a form with a slightly better memory than most of us have before coffee — depending on the day, that may not be a terribly high bar, but still.</p><p>This is especially handy for forms that are a little more involved than a simple email signup. Job applications, checkout flows, support requests, and multi-question onboarding forms all benefit from a little resilience. So do the people filling them out, who generally have better things to do than retype the same answers because a tab got squirrelly.</p><h2 id="what-actually-gets-saved%3F" tabindex="-1"><a class="header-anchor" href="#what-actually-gets-saved%3F" aria-hidden="true">#</a> What actually gets saved?</h2><p><code>form-saver</code> supports the controls most of us reach for every day:</p><ul><li>Text-style <code>input</code> fields</li><li><code>textarea</code> elements,</li><li><code>select</code> elements (including multi-selects), and</li><li><code>checkbox</code> and <code>radio</code> controls.</li></ul><p>File inputs are intentionally excluded.</p><p>Because the component works in light DOM, your form remains your form. Your labels, validation, layout, and CSS continue to work exactly as they did before. <code>form-saver</code> just adds a bit of memory and, ideally, cuts down on a few muttered curses.</p><h2 id="want-to-keep-a-few-fields-after-submit%3F" tabindex="-1"><a class="header-anchor" href="#want-to-keep-a-few-fields-after-submit%3F" aria-hidden="true">#</a> Want to keep a few fields after submit?</h2><p>In many cases, clearing everything after a successful submission is the right call. Sometimes, though, it makes sense to keep a few details around. Maybe you want to preserve a visitor’s name and email address on a contact form while clearing the message body. That way they do not have to keep retyping the boring bits. Nobody wakes up excited to enter their email address for the fourth time.</p><p>That is what the <code>retain</code> attribute is for:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">retain</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name email<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>After a successful submission, <code>name</code> and <code>email</code> stick around, but <code>message</code> gets cleared. Simple, sensible, and less likely to leave someone staring at your form like it just betrayed them personally.</p><h2 id="better-yet%2C-let-users-decide" tabindex="-1"><a class="header-anchor" href="#better-yet%2C-let-users-decide" aria-hidden="true">#</a> Better yet, let users decide</h2><p>Persisting form data can be incredibly helpful, but there is a human side to this too. Just because we <em>can</em> keep someone’s information around does not necessarily mean we <em>should</em> do it without asking. That is where <code>retain-choice</code> comes in. It lets you be useful without getting presumptuous.</p><p>Add it alongside <code>retain</code> and <code>form-saver</code> will inject an opt-in checkbox for the user. Nice and easy:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">retain</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name email<span class="token punctuation">”</span></span><span class="token attr-name">retain-choice</span><span class="token attr-name">retain-choice-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Store my contact information for later<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>By default, that checkbox is inserted just before the first submit control. If the user leaves it unchecked, the retained fields are cleared along with everything else after submit. If they opt in, those selected fields remain. Their call, as it should be. Gotta love a little informed consent.</p><p>Need to place that control somewhere more appropriate in your layout? Use <code>retain-choice-container</code> to point to a CSS selector:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">retain</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>name email<span class="token punctuation">”</span></span><span class="token attr-name">retain-choice</span><span class="token attr-name">retain-choice-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Remember my details next time<span class="token punctuation">”</span></span><span class="token attr-name">retain-choice-container</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>.form-footer<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/contact<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>…<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>form-footer<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Send<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>That gives you a lot more control over the experience without making you build the retention UI yourself.</p><h2 id="need-a-custom-storage-key%3F" tabindex="-1"><a class="header-anchor" href="#need-a-custom-storage-key%3F" aria-hidden="true">#</a> Need a custom storage key?</h2><p>By default, <code>form-saver</code> derives its storage key from the wrapped form’s method and action, which is usually exactly what you want. It keeps different forms from stepping on one another and keeps the setup nice and boring. Boring is good.</p><p>If you need something more explicit, you can provide your own <code>storage-key</code>:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-saver</span><span class="token attr-name">storage-key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>checkout:shipping-address<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/checkout/shipping<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Street Address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>street-address<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>street-address<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Postal Code<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>postal-code<span class="token punctuation">”</span></span><span class="token attr-name">autocomplete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>postal-code<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>submit<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Continue<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-saver</span><span class="token punctuation">&gt;</span></span></code></pre><p>This is useful when a form’s URL is not stable or when you want multiple views to intentionally share the same saved state. Sometimes explicit is just easier. Sometimes it is the only way to stay sane.</p><h2 id="want-to-drive-it-yourself%3F" tabindex="-1"><a class="header-anchor" href="#want-to-drive-it-yourself%3F" aria-hidden="true">#</a> Want to drive it yourself?</h2><p>If you need more direct control, the component exposes a few methods:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> saver <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-saver”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Persist the current state</span>saver<span class="token punctuation">.</span><span class="token function">saveFormState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Restore previously saved values</span>saver<span class="token punctuation">.</span><span class="token function">restoreFormState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Clear out anything stored for this form</span>saver<span class="token punctuation">.</span><span class="token function">clearSavedData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>That can be useful when you want to pair it with your own UI, analytics, or some custom workflow around save and restore. Or when you just like being the one driving and do not fully trust anything labeled “automatic.”</p><h2 id="progressive-enhancement%2C-as-usual" tabindex="-1"><a class="header-anchor" href="#progressive-enhancement%2C-as-usual" aria-hidden="true">#</a> Progressive enhancement, as usual</h2><p>This component follows a pattern I am always going to favor: start with a perfectly ordinary form, then layer on the enhancement. If JavaScript fails, the form still works. Users can still fill it out and submit it. They just will not get the recovery behavior. Annoying, perhaps, but not catastrophic. And that is very much the point.</p><p>That’s a pretty good trade-off.</p><p>And because saved values are only cleared after a successful submit flow, you do not lose everything just because client-side validation blocked submission or some other script got clever at exactly the wrong moment. That matters. A lot of “smart” form experiences are only smart right up until they are not.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>If you want to kick the tires, I put together <a href="https://aarongustafson.github.io/form-saver/demo/">a live demo</a> with examples of the retention options as well:</p><figure id="fig-2026-04-20-01" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-saver/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>The project is available on <a href="https://github.com/aarongustafson/form-saver">GitHub</a>, and you can install it from npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-saver</code></pre><p>If you want the easiest path, just import it and let the component register itself:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/form-saver”</span><span class="token punctuation">;</span></code></pre><p>If you would rather define it yourself, you can import the class directly:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> FormSaverElement <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/form-saver/form-saver.js”</span><span class="token punctuation">;</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">“form-saver”</span><span class="token punctuation">,</span> FormSaverElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Either way, you wind up with a more forgiving form experience and a little less needless frustration for the people filling it out. Which, in my book, is a pretty solid deal. The bar for delight in forms is often just “don’t make me do that again,” and honestly, I’ll take it.</p>]]></content><amg:twitter><![CDATA[Browsers crash. Tabs close. Life happens. Here’s a web component that saves form progress so your users don’t have to start over from scratch.]]></amg:twitter><amg:summary><![CDATA[<p>Few things are more annoying than losing your progress halfway through a form. Maybe the browser crashes. Maybe the tab gets closed. Maybe your kid yells from the other room and you come back three hours later wondering why you ever thought now was a good time to fill out a mortgage application. Whatever the cause, <code>form-saver</code> makes those interruptions a lot less obnoxious. Which is nice, because forms are usually annoying enough on their own.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Few things are more annoying than losing your progress halfway through a form. Maybe the browser crashes. Maybe the tab gets closed. Maybe your kid yells from the other room and you come back three hours later wondering why you ever thought now was a good time to fill out a mortgage application. Whatever the cause, <code>form-saver</code> makes those interruptions a lot less obnoxious. Which is nice, because forms are usually annoying enough on their own.</p>]]></summary><category term="web components" /><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web forms" /><category term="user experience" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/accessibility-assistant-for-figma-v52/</id><title type="html"><![CDATA[Accessibility Assistant for Figma v52]]></title><link href="https://www.aaron-gustafson.com/notebook/accessibility-assistant-for-figma-v52/" rel="alternate" type="text/html" /><published>2026-02-20T23:27:28Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<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 some details on why this is a monumental release for us.</p><p>We’re in the process of a major overhaul to this plugin. There was a lot of infrastructural work to do to modernize the plugin and set the stage for a host of new features to make designers more productive when it comes to making their designs more accessible. This release incorporates a lot of that foundational work, notably:</p><ul><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><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><li>Annotations are now managed in a single UI rather than being separated, based on whether they impact focus order. This means you don’t need to jump back &amp; forth between tools to properly annotate your designs.</li><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></ul><p>We also fixed bugs related to duplicating layers. You can now copy layers and the annotations will go along for the ride, becoming a new Annotation Set. Similarly, you can now duplicate pages and the annotations — which are page-bound — will be re-generated. It’s worth noting that this may take some time on particularly large pages.</p><p>This release has been a long time coming, but I’m incredibly proud of the team that’s been working so diligently on this, particularly Ashish Singh from HCL and Michael Fairchild, Scott O’Hara, and Ben Truelove from Microsoft. Their attention to detail and encyclopedic knowledge of accessibility has been instrumental in getting this project to the place that it is.</p><p>And there’s more to come!</p>]]></content><amg:twitter><![CDATA[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.]]></amg:twitter><amg:summary><![CDATA[<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 some details on why this is a monumental release for us.</p>]]></amg:summary><summary type="html"><![CDATA[<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 some details on why this is a monumental release for us.</p>]]></summary><category term="accessibility" /><category term="design" /><category term="inclusive design" /><category term="Microsoft" /><category term="user experience" /><category term="WAI-ARIA" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/repeatable-form-fields-made-simple/</id><title type="html"><![CDATA[Repeatable Form Fields Made Simple]]></title><link href="https://www.aaron-gustafson.com/notebook/repeatable-form-fields-made-simple/" rel="alternate" type="text/html" /><published>2026-01-31T00:04:36Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Sometimes you need users to provide multiple instances of the same information—multiple email addresses, phone numbers, team members, or emergency contacts. The <code>form-repeatable</code> web component makes this straightforward, handling field duplication, automatic renumbering, and seamless form submission via the ElementInternals API.</p><p>All you need to do is provide a single field group and the component handles the rest:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>stop-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Stop 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>stop-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>stops[]<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>The <code>form-repeatable</code> component treats its first child as a template and injects a <code>button</code> that allows users to repeat the field. When users click “Add Another” (the default “add” button text), the following happens:</p><ol><li>The template is cloned,</li><li>Any numbers are auto-incremented (“Stop 1” → “Stop 2”, <code>stop-1</code> → <code>stop-2</code>),</li><li>A new group is added to the component,</li><li>A “remove” button is added when there’s more than the minimum number of groups (1 by default), and</li><li>Form values update automatically via <code>ElementInternals</code>.</li></ol><p>That last piece is crucial. The plugin is a fully-participating member in the parent form:</p><ul><li>All inputs are collected and submitted automatically</li><li>Values are added to <code>FormData</code></li><li>In-built form reset is respected</li><li>A form’s disabled state is respected</li></ul><p>No special handling required — it works like any native form control.</p><h2 id="need-customized-buttons%3F-you-bet!" tabindex="-1"><a class="header-anchor" href="#need-customized-buttons%3F-you-bet!" aria-hidden="true">#</a> Need customized buttons? You bet!</h2><p>If you don’t like the default text or your site is in another language — no biggie! You can define your own button labels using the <code>add-label</code> and <code>remove-label</code> attributes:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token attr-name">add-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Add Another Item<span class="token punctuation">”</span></span><span class="token attr-name">remove-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Delete<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>item-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Item 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>item-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>items[]<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>With that simple change, the add button reads “Add Another Item” and each remove button reads “Delete”. To improve the experience for screen reader users, the <code>remove-label</code> value is combined with the associated label/legend to create accessible names like “Delete Item 1” which is far more helpful.</p><h2 id="already-have-values-to-show%3F-no-problem." tabindex="-1"><a class="header-anchor" href="#already-have-values-to-show%3F-no-problem." aria-hidden="true">#</a> Already have values to show? No problem.</h2><p>If your form needs to start with multiple groups already filled in, just provide them as child elements:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>tel<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phones[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>555-0100<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-2<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-2<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>tel<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phones[]<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>555-0101<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-3<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone 3<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-3<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>tel<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phones[]<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">&gt;</span></span></code></pre><p>All the children will become groups managed by the component and their existing values are preserved. Perfect progressive enhancement!</p><h2 id="need-to-do-something-a-little-more-complex%3F-i-got-you." tabindex="-1"><a class="header-anchor" href="#need-to-do-something-a-little-more-complex%3F-i-got-you." aria-hidden="true">#</a> Need to do something a little more complex? I got you.</h2><p>You’re not limited to repeating a single field. Each group can contain multiple, related fields. Here’s an example with a <code>fieldset</code> for guest information:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>Guest 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-name-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-name-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-name-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>guest-email-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>When this gets picked up by the component, the whole <code>fieldset</code> will become the template. When users add new Guests, all of the numeric values — whether in text or attributes — increment automatically when new groups are added. So in this case, the <code>legend</code> will update, as will the <code>for</code> attribute on the <code>label</code> and <code>id</code> and <code>name</code> attributes on the <code>input</code>.</p><h2 id="need-to-constrain-the-responses%3F-you-got-it." tabindex="-1"><a class="header-anchor" href="#need-to-constrain-the-responses%3F-you-got-it." aria-hidden="true">#</a> Need to constrain the responses? You got it.</h2><p>Use the <code>min</code> and <code>max</code> attributes to control the number of allowed groups:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token attr-name">min</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>2<span class="token punctuation">”</span></span><span class="token attr-name">max</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>5<span class="token punctuation">”</span></span><span class="token attr-name">add-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Add Team Member<span class="token punctuation">”</span></span><span class="token attr-name">remove-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Remove<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>member-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Team Member 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>member-1<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>members[]<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>This creates a component that:</p><ul><li>Starts with 1 member,</li><li>Requires adding new members until the <code>min</code> threshold (2) is met,</li><li>Cannot have fewer than 2 team members,</li><li>Cannot have more than 5 team members, and</li><li>Uses custom button labels.</li></ul><p>The remove buttons are not shown when at the minimum threshold (1 by default) and the add button disappears when you hit the maximum.</p><h2 id="prefer-an-explicit-template%3F-bring-it!" tabindex="-1"><a class="header-anchor" href="#prefer-an-explicit-template%3F-bring-it!" aria-hidden="true">#</a> Prefer an explicit <code>template</code>? Bring it!</h2><p>This component can accept a <code>template</code> element containing the fields you want to repeat. Just drop in <code>{n}</code> placeholders where you want the sequential numbers to appear:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-repeatable</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email-{n}<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Email {n}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email-{n}<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>emails[]<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-repeatable</span><span class="token punctuation">&gt;</span></span></code></pre><p>When hoisted into the component, the <code>template</code> element is removed from the light DOM and used internally.</p><h2 id="here%E2%80%99s-what-you-need-to-know-about-styling-it" tabindex="-1"><a class="header-anchor" href="#here%E2%80%99s-what-you-need-to-know-about-styling-it" aria-hidden="true">#</a> Here’s what you need to know about styling it</h2><p>The component uses Shadow DOM to encapsulate its internal structure, but you can style it using CSS parts and custom properties. It also adopts your global styles automatically.</p><p>The component uses CSS Grid by default:</p><ul><li><strong>Two columns</strong>: Content in column 1, remove buttons aligned inline end in column 2</li><li><strong>Subgrid</strong>: Each group uses <code>subgrid</code> to align with parent grid</li><li><strong>Add button</strong>: Appears below all groups</li></ul><p>You can use CSS parts to style the buttons and field groups. Here are some examples:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token comment">/* Style all buttons <em>/</span><span class="token selector">form-repeatable::part(button)</span><span class="token punctuation">{</span><span class="token property">padding</span><span class="token punctuation">:</span> 0.5rem 1rem<span class="token punctuation">;</span><span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span><span class="token property">cursor</span><span class="token punctuation">:</span> pointer<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style the add button <em>/</span><span class="token selector">form-repeatable::part(add-button)</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #28a745<span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style remove buttons <em>/</span><span class="token selector">form-repeatable::part(remove-button)</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #dc3545<span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Customize the grid layout <em>/</span><span class="token selector">form-repeatable::part(groups)</span><span class="token punctuation">{</span><span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 1fr auto<span class="token punctuation">;</span><span class="token property">gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Style each group */</span><span class="token selector">form-repeatable::part(group)</span><span class="token punctuation">{</span><span class="token property">padding</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token property">background</span><span class="token punctuation">:</span> #f8f9fa<span class="token punctuation">;</span><span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span><span class="token property">margin-bottom</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>Available parts:</p><ul><li><code>groups</code> - Container for all groups (CSS grid by default)</li><li><code>group</code> - Each repeatable group wrapper</li><li><code>content</code> - Container for group’s fields</li><li><code>group-controls</code> - Container for the remove button</li><li><code>controls</code> - Container for the add button</li><li><code>button</code> - All buttons</li><li><code>add-button</code> - The add button</li><li><code>remove-button</code> - All remove buttons</li></ul><h2 id="want-to-keep-a-watchful-eye%3F-you%E2%80%99re-extra%2C-but-sure." tabindex="-1"><a class="header-anchor" href="#want-to-keep-a-watchful-eye%3F-you%E2%80%99re-extra%2C-but-sure." aria-hidden="true">#</a> Want to keep a watchful eye? You’re extra, but sure.</h2><p>You can listen for when groups are added or removed and run your own custom code:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> repeatable <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-repeatable”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>repeatable<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-repeatable:added”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Group added. Total groups:”</span><span class="token punctuation">,</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>groupCount<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>repeatable<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-repeatable:removed”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Group removed. Total groups:”</span><span class="token punctuation">,</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>groupCount<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="go-with-the-progressive-enhancement-flow" tabindex="-1"><a class="header-anchor" href="#go-with-the-progressive-enhancement-flow" aria-hidden="true">#</a> Go with the progressive enhancement flow</h2><p>If JavaScript fails, users see the initial field group(s) and can fill them in. They can’t add more, but nothing breaks. Make sure your minimum count accommodates users without JavaScript.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>Explore <a href="https://aarongustafson.github.io/form-repeatable/demo/">the demo</a> with various examples:</p><figure id="fig-2025-12-06-07" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-repeatable/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it-now" tabindex="-1"><a class="header-anchor" href="#grab-it-now" aria-hidden="true">#</a> Grab it now</h2><p>Check out the project on <a href="https://github.com/aarongustafson/form-repeatable">GitHub</a>. Install via npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-repeatable</code></pre><p>Import and go:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/form-repeatable”</span><span class="token punctuation">;</span></code></pre><p>This single component instance manages all your repeatable field groups with native form participation — no framework required.</p>]]></content><amg:twitter><![CDATA[Add repeatable form field groups with automatic numbering and native form participation.]]></amg:twitter><amg:summary><![CDATA[<p>Sometimes you need users to provide multiple instances of the same information—multiple email addresses, phone numbers, team members, or emergency contacts. The <code>form-repeatable</code> web component makes this straightforward, handling field duplication, automatic renumbering, and seamless form submission via the ElementInternals API.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Sometimes you need users to provide multiple instances of the same information—multiple email addresses, phone numbers, team members, or emergency contacts. The <code>form-repeatable</code> web component makes this straightforward, handling field duplication, automatic renumbering, and seamless form submission via the ElementInternals API.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-starter-template/</id><title type="html"><![CDATA[A Production-Ready Web Component Starter Template]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-starter-template/" rel="alternate" type="text/html" /><published>2026-01-02T00:06:37Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Creating a new web component from scratch involves a lot of boilerplate—testing setup, build configuration, linting, CI/CD, documentation structure, and more. After building — and refining/rebuilding — numerous web components, I’ve distilled all that work into a starter template that lets you focus on your component’s functionality rather than project setup.</p><p>The <a href="https://github.com/aarongustafson/web-component-starter">Web Component Starter Template</a> is based on the architecture and patterns I’ve refined across my web component work, incorporating <a href="https://web.dev/articles/custom-elements-best-practices">Google’s Custom Element Best Practices</a> and advice from other web components practitioners including the always-brilliant <a href="https://daverupert.com/">Dave Rupert</a>.</p><h2 id="what%E2%80%99s-included" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-included" aria-hidden="true">#</a> What’s included</h2><p>The template provides everything you need to create a production-ready web component:</p><ul><li><strong>Interactive setup wizard</strong> that scaffolds everything for your component.</li><li><strong>Multiple import patterns</strong> supporting both auto-define and manual registration.</li><li><strong>Demo pages</strong> for development, documentation, and CDN examples.</li><li><strong>Code quality tools</strong> including ESLint and Prettier with sensible defaults.</li><li><strong>Modern testing setup</strong> with Vitest, Happy DOM, and coverage reporting.</li><li><strong>CI/CD workflows</strong> for GitHub Actions with automated testing and npm publishing.</li><li><strong>Publishing ready</strong> with proper npm package configuration and OIDC support.</li></ul><h2 id="quick-start-with-interactive-setup" tabindex="-1"><a class="header-anchor" href="#quick-start-with-interactive-setup" aria-hidden="true">#</a> Quick start with interactive setup</h2><p>Getting started is straightforward. If you’re a GitHub user, you can create a new repository directly from the template. Alternatively, clone it locally:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> clone <a href="https://github.com/aarongustafson/web-component-starter.git">https://github.com/aarongustafson/web-component-starter.git</a> my-component<span class="token builtin class-name">cd</span> my-component<span class="token function">npm</span><span class="token function">install</span><span class="token function">npm</span> run setup</code></pre><p>The setup wizard asks for your component name and description, then automatically:</p><ul><li>Renames all files based on your component name,</li><li>Updates all code and configuration templates with your details,</li><li>Generates a proper README from the included template,</li><li>Cleans up all template-specific files, and</li><li>Initializes the git repository.</li></ul><p>You’re left with a fully scaffolded repository, ready for you to develop your component.</p><h2 id="flexible-import-patterns" tabindex="-1"><a class="header-anchor" href="#flexible-import-patterns" aria-hidden="true">#</a> Flexible import patterns</h2><p>One of the key features is support for multiple registration patterns. Users of your component can choose what works best:</p><p><strong>Manual registration for full control:</strong></p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> ComponentNameElement <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">‘@yourscope/component-name’</span><span class="token punctuation">;</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">‘my-custom-name’</span><span class="token punctuation">,</span> ComponentNameElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p><strong>Auto-define for convenience:</strong></p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">‘@yourscope/component-name/define.js’</span><span class="token punctuation">;</span></code></pre><p><strong>Or call the helper directly:</strong></p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> defineComponentName <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">‘@yourscope/component-name/define.js’</span><span class="token punctuation">;</span><span class="token function">defineComponentName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>The auto-define approach includes guards to ensure it only runs in browser environments and checks if <code>customElements</code> is available, making it safe for server-side rendered (SSR) scenarios.</p><h2 id="testing-made-easy" tabindex="-1"><a class="header-anchor" href="#testing-made-easy" aria-hidden="true">#</a> Testing made easy</h2><p>The template includes a comprehensive testing setup using Vitest:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> describe<span class="token punctuation">,</span> it<span class="token punctuation">,</span> expect <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">‘vitest’</span><span class="token punctuation">;</span><span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">‘MyComponent’</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">‘should render’</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">const</span> el <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">‘my-component’</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token function">expect</span><span class="token punctuation">(</span>el<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBeInstanceOf</span><span class="token punctuation">(</span>HTMLElement<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Happy DOM provides a lightweight browser environment, and the included scripts support:</p><ul><li>Watch mode for development: <code>npm test</code></li><li>Single run for CI: <code>npm run test:run</code></li><li>Interactive UI: <code>npm run test:ui</code></li><li>Coverage reports: <code>npm run test:coverage</code></li></ul><h2 id="automated-publishing-with-oidc" tabindex="-1"><a class="header-anchor" href="#automated-publishing-with-oidc" aria-hidden="true">#</a> Automated publishing with OIDC</h2><p>The template is configured for secure automated publishing to npm using OpenID Connect (OIDC), which is more secure than long-lived tokens. After you manually publish the first version and configure OIDC on npm, create a GitHub release and the workflow handles publishing automatically.</p><p>Manual publishing is still supported if you prefer that approach.</p><h2 id="following-best-practices" tabindex="-1"><a class="header-anchor" href="#following-best-practices" aria-hidden="true">#</a> Following best practices</h2><p>The template bakes in best practices from the start:</p><ul><li>Shadow DOM with proper encapsulation</li><li>Custom Elements v1 API</li><li>Reflection of properties to attributes</li><li>Lifecycle callbacks used appropriately</li><li>Accessible patterns and ARIA support</li><li>Progressive enhancement approach</li></ul><p>The included <a href="https://github.com/aarongustafson/web-component-starter/blob/main/WEB-COMPONENTS-BEST-PRACTICES.md"><code>WEB-COMPONENTS-BEST-PRACTICES.md</code> document</a> explains the reasoning behind each pattern, making it a learning resource as well as a starter template.</p><h2 id="why-i-built-this" tabindex="-1"><a class="header-anchor" href="#why-i-built-this" aria-hidden="true">#</a> Why I built this</h2><p>After creating components like <a href="https://github.com/aarongustafson/form-obfuscator">form-obfuscator</a>, <a href="https://github.com/aarongustafson/tabbed-interface">tabbed-interface</a>, and several others, I found myself copying and adapting the same project structure each time. This template captures those patterns so I — and now you — can start building components faster.</p><p>If you build something with it, I’d love to hear about it!</p>]]></content><amg:twitter><![CDATA[Start building web components the right way with this production-ready template.]]></amg:twitter><amg:summary><![CDATA[<p>Creating a new web component from scratch involves a lot of boilerplate—testing setup, build configuration, linting, CI/CD, documentation structure, and more. After building — and refining/rebuilding — numerous web components, I’ve distilled all that work into a starter template that lets you focus on your component’s functionality rather than project setup.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Creating a new web component from scratch involves a lot of boilerplate—testing setup, build configuration, linting, CI/CD, documentation structure, and more. After building — and refining/rebuilding — numerous web components, I’ve distilled all that work into a starter template that lets you focus on your component’s functionality rather than project setup.</p>]]></summary><category term="web components" /><category term="JavaScript" /><category term="open source" /><category term="developer tools" /><category term="best practices" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/fullscreen-video-and-iframes-made-easy/</id><title type="html"><![CDATA[Fullscreen Video and Iframes Made Easy]]></title><link href="https://www.aaron-gustafson.com/notebook/fullscreen-video-and-iframes-made-easy/" rel="alternate" type="text/html" /><published>2025-12-29T17:17:27Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Adding fullscreen capabilities to videos and embedded iframes shouldn’t require wrestling with prefixed APIs or managing focus states. The <code>fullscreen-control</code> web component handles all of that for you — just wrap it around the element. The component handles the rest as a discrete progressive enhancement.</p><h2 id="easy-peasy" tabindex="-1"><a class="header-anchor" href="#easy-peasy" aria-hidden="true">#</a> Easy-peasy</h2><p>Here’s a simple example using a <code>video</code> element:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fullscreen-control</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>video</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>video.mp4<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>video</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fullscreen-control</span><span class="token punctuation">&gt;</span></span></code></pre><p>With that in place, the component</p><ul><li>Adds a styleable button for launching fullscreen control over the contained element,</li><li>Handles browser prefixes as needed,</li><li>Manages focus automatically,</li><li>Rigs up the necessary keyboard events (e.g. <kbd>Escape</kbd> to exit), and</li><li>Assigns the relevant ARIA attributes.</li></ul><p>The component uses light DOM, so your <code>video</code> stays in the regular DOM tree and all your existing CSS continues to work.</p><h2 id="fullscreen-iframes" tabindex="-1"><a class="header-anchor" href="#fullscreen-iframes" aria-hidden="true">#</a> Fullscreen iframes</h2><p>Need to embed a YouTube video, slide deck, or code demo? The component works with <code>iframe</code> elements too:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fullscreen-control</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>iframe</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span><a href="https://www.youtube.com/embed/dQw4w9WgXcQ">https://www.youtube.com/embed/dQw4w9WgXcQ</a><span class="token punctuation">”</span></span><span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>560<span class="token punctuation">”</span></span><span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>315<span class="token punctuation">”</span></span><span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>YouTube video player<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>iframe</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fullscreen-control</span><span class="token punctuation">&gt;</span></span></code></pre><p>The component automatically adds the necessary <code>allow=&quot;fullscreen&quot;</code> and <code>allowfullscreen</code> attributes, including prefixed versions for broader compatibility.</p><h2 id="customizable-button-text" tabindex="-1"><a class="header-anchor" href="#customizable-button-text" aria-hidden="true">#</a> Customizable <code>button</code> text</h2><p>You can change the <code>button</code> label to match your site’s language or writing style by setting the <code>button-text</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fullscreen-control</span><span class="token attr-name">button-text</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>全画面表示<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>video</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>video.mp4<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>video</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fullscreen-control</span><span class="token punctuation">&gt;</span></span></code></pre><p>The default button label is “View fullscreen,” but you can use this attribute to customize it to anything you like. You can even dynamically inject the accessible name of the contained element, using the <code>{name}</code> token. For example:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fullscreen-control</span><span class="token attr-name">button-text</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>View {name} fullscreen<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>video</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>video.mp4<span class="token punctuation">”</span></span><span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Product demo<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>video</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fullscreen-control</span><span class="token punctuation">&gt;</span></span></code></pre><p>This creates a <code>button</code> with the text “View Product demo fullscreen”. The component looks for <code>aria-label</code>, <code>title</code>, or other native naming on the wrapped element and uses that to make the <code>button</code> contextual.</p><h2 id="distinct-screen-reader-labels" tabindex="-1"><a class="header-anchor" href="#distinct-screen-reader-labels" aria-hidden="true">#</a> Distinct screen reader labels</h2><p>If you want the visible label and accessible button name to differ, use the <code>button-label</code> attribute. Like <code>button-text</code>, it can also inject the accessible name of the controlled element using the <code>{name}</code> token:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fullscreen-control</span><span class="token attr-name">button-text</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Fullscreen<span class="token punctuation">”</span></span><span class="token attr-name">button-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>View {name} in fullscreen mode<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>iframe</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span><a href="https://example.com">https://example.com</a><span class="token punctuation">”</span></span><span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Product teaser<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>iframe</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fullscreen-control</span><span class="token punctuation">&gt;</span></span></code></pre><p>This code will generate a <code>button</code> that visually reads “Fullscreen”, but is announced as “View Product teaser in fullscreen mode” to screen readers. In mode cases, <code>button-text</code> will suffice, but this option is available if you need to distinguish the buttons of multiple fullscreen controls from one another and don’t have visual space to display their accessible names.</p><h2 id="focus-management" tabindex="-1"><a class="header-anchor" href="#focus-management" aria-hidden="true">#</a> Focus management</h2><p>If users activate fullscreen using the button, focus will automatically return to the button upon exiting fullscreen. This ensures keyboard users don’t lose their place.</p><h2 id="need-more-control%3F" tabindex="-1"><a class="header-anchor" href="#need-more-control%3F" aria-hidden="true">#</a> Need more control?</h2><p>Want to manage the component yourself? The component exposes three methods:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> control <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">“fullscreen-control”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Enter fullscreen</span><span class="token keyword">await</span> control<span class="token punctuation">.</span><span class="token function">enterFullscreen</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Exit fullscreen</span><span class="token keyword">await</span> control<span class="token punctuation">.</span><span class="token function">exitFullscreen</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Toggle fullscreen state</span>control<span class="token punctuation">.</span><span class="token function">toggleFullscreen</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>These handle all the browser prefixes and error handling for you.</p><p>There are also a set of events you can tap into when the fullscreen state changes:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> control <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">“fullscreen-control”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>control<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“fullscreen-control:enter”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Entered fullscreen mode”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>control<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“fullscreen-control:exit”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Exited fullscreen mode”</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>These events give you the ability to pause other media, track analytics, and the like.</p><h2 id="style-the-button" tabindex="-1"><a class="header-anchor" href="#style-the-button" aria-hidden="true">#</a> Style the button</h2><p>Since the component uses light DOM, you can style the button directly with CSS:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token selector">fullscreen-control button</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #ff6b6b<span class="token punctuation">;</span><span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><span class="token property">border</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><span class="token property">padding</span><span class="token punctuation">:</span> 0.75rem 1.5rem<span class="token punctuation">;</span><span class="token property">border-radius</span><span class="token punctuation">:</span> 20px<span class="token punctuation">;</span><span class="token property">font-weight</span><span class="token punctuation">:</span> bold<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token selector">fullscreen-control button:hover</span><span class="token punctuation">{</span><span class="token property">background</span><span class="token punctuation">:</span> #ff5252<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>The button is positioned absolutely by default (top-right corner), but you can adjust this with CSS custom properties:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token selector">fullscreen-control</span><span class="token punctuation">{</span><span class="token property">–fullscreen-control-button-inset-block-start</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token property">–fullscreen-control-button-inset-inline-end</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>This uses logical properties, so it adapts automatically to different writing modes.</p><h2 id="installation" tabindex="-1"><a class="header-anchor" href="#installation" aria-hidden="true">#</a> Installation</h2><p>Install via npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/fullscreen-control</code></pre><p>Then import it in your JavaScript:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/fullscreen-control/define.js”</span><span class="token punctuation">;</span></code></pre><p>Or load it from a CDN for quick prototyping:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>module<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> defineFullscreenControl <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“<a href="https://unpkg.com/@aarongustafson/fullscreen-control@latest/define.js?module">https://unpkg.com/@aarongustafson/fullscreen-control@latest/define.js?module</a>”</span><span class="token punctuation">;</span><span class="token function">defineFullscreenControl</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span></code></pre><h2 id="browser-support" tabindex="-1"><a class="header-anchor" href="#browser-support" aria-hidden="true">#</a> Browser support</h2><p>The component uses modern web standards (Custom Elements v1, ES Modules) and handles browser-prefixed fullscreen APIs internally. For older browsers, you may need polyfills, but the component gracefully handles missing APIs with console warnings rather than breaking your page.</p><h2 id="demo-and-source-code" tabindex="-1"><a class="header-anchor" href="#demo-and-source-code" aria-hidden="true">#</a> Demo and source code</h2><p>Check out the <a href="https://aarongustafson.github.io/fullscreen-control/demo/">live demo</a> to see all the features in action, or grab the code from <a href="https://github.com/aarongustafson/fullscreen-control">GitHub</a>.</p>]]></content><amg:twitter><![CDATA[Add fullscreen controls to videos and iframes with progressive enhancement. One wrapper, zero hassle.]]></amg:twitter><amg:summary><![CDATA[<p>Adding fullscreen capabilities to videos and embedded iframes shouldn’t require wrestling with prefixed APIs or managing focus states. The <code>fullscreen-control</code> web component handles all of that for you — just wrap it around the element. The component handles the rest as a discrete progressive enhancement.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Adding fullscreen capabilities to videos and embedded iframes shouldn’t require wrestling with prefixed APIs or managing focus states. The <code>fullscreen-control</code> web component handles all of that for you — just wrap it around the element. The component handles the rest as a discrete progressive enhancement.</p>]]></summary><category term="web components" /><category term="progressive enhancement" /><category term="HTML" /><category term="video" /><category term="accessibility" /><category term="media" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/dynamic-datalist-autocomplete-from-an-api/</id><title type="html"><![CDATA[Dynamic Datalist: Autocomplete from an API]]></title><link href="https://www.aaron-gustafson.com/notebook/dynamic-datalist-autocomplete-from-an-api/" rel="alternate" type="text/html" /><published>2025-12-16T19:46:29Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>HTML’s <code>datalist</code> element provides native autocomplete functionality, but it’s entirely static—you have to know all the options up front. The <code>dynamic-datalist</code> web component solves this by fetching suggestions from an API endpoint as users type, giving you the benefits of native autocomplete with the flexibility of dynamic data.</p><p>This component is a modern replacement for <a href="https://github.com/easy-designs/jquery.easy-predictive-typing.js">my old jQuery predictive typing plugin</a>. I’ve reimagined it as a standards-based web component.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>To use the component, wrap it around your <code>input</code> field and specify an endpoint:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Search<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Type to search…<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>As users type, the component makes GET requests to that endpoint, passing in the typed value as the “query” parameter (e.g., <code>/api/search?query=WHAT_THE_USER_TYPED</code>). The response from the endpoint is used to populates a dynamic <code>datalist</code> element with the results.</p><p>The structure of the response should be JSON with an <code>options</code> array of string values:</p><pre class="language-json" tabindex="0"><code class="language-json"><span class="token punctuation">{</span><span class="token property">“options”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token string">“option 1”</span><span class="token punctuation">,</span><span class="token string">“option 2”</span><span class="token punctuation">,</span><span class="token string">“option 3”</span><span class="token punctuation">]</span><span class="token punctuation">}</span></code></pre><h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works" aria-hidden="true">#</a> How it works</h2><p>Under the hood, the component:</p><ol><li>Adopts (or creates) a <code>datalist</code> element for your <code>input</code>,</li><li>Listens for “input” events,</li><li>Debounces requests (waiting at least 250ms) to avoid overwhelming your API,</li><li>Sends requests to your endpoint with the current value of the <code>input</code>,</li><li>Reads back the JSON response,</li><li>Updates the <code>datalist</code><code>option</code> elements, and</li><li>Dispatches the update event.</li></ol><p>All of this happens transparently—users just see autocomplete suggestions appearing as they type.</p><h2 id="need-post%3F" tabindex="-1"><a class="header-anchor" href="#need-post%3F" aria-hidden="true">#</a> Need POST?</h2><p>You can change the submission method via the <code>method</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/lookup<span class="token punctuation">”</span></span><span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>post<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>lookup<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Lookup<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>lookup<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>lookup<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>This sends a POST request with a JSON body: <code>{ &quot;query&quot;: &quot;…&quot; }</code>. Currently GET and POST are supported, but I could add more if folks want them.</p><h2 id="custom-variable-names" tabindex="-1"><a class="header-anchor" href="#custom-variable-names" aria-hidden="true">#</a> Custom variable names</h2><p>As I mentioned, the component uses “query” as the parameter name by default, but you can easily change it via the <code>key</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/terms<span class="token punctuation">”</span></span><span class="token attr-name">key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>term<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Term search<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>search<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>term<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>This sends the GET request <code>/api/terms?term=…</code>.</p><h2 id="working-with-existing-datalists" tabindex="-1"><a class="header-anchor" href="#working-with-existing-datalists" aria-hidden="true">#</a> Working with existing datalists</h2><p>If your <code>input</code> already has a <code>datalist</code> defined, the component will inherit it and replace the existing options with the fetched results, which makes for a nice progressive enhancement:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>dynamic-datalist</span><span class="token attr-name">endpoint</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>/api/cities<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>city<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>City<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>city<span class="token punctuation">”</span></span><span class="token attr-name">list</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>cities-list<span class="token punctuation">”</span></span><span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Type a city…<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>datalist</span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>cities-list<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>New York<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>Los Angeles<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span><span class="token punctuation">&gt;</span></span>Chicago<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>datalist</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>dynamic-datalist</span><span class="token punctuation">&gt;</span></span></code></pre><p>Users see the pre-populated cities immediately, and as they type, API results supplement the list. If JavaScript fails or the web component doesn’t load, users still get the static options. Nothing breaks.</p><h2 id="event-handling" tabindex="-1"><a class="header-anchor" href="#event-handling" aria-hidden="true">#</a> Event handling</h2><p>If you want to tap into the component’s event system, it fires three custom events:</p><ul><li><code>dynamic-datalist:ready</code> - Fired when the component initializes</li><li><code>dynamic-datalist:update</code> - Fired when the <code>datalist</code> is updated with new options</li><li><code>dynamic-datalist:error</code> - Fired when an error occurs fetching data</li></ul><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> element <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:ready”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Component ready:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:update”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Options updated:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>element<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist:error”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">“Error:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Each event provides helpful <code>detail</code> objects with references to the <code>input</code>, <code>datalist</code>, and other relevant data.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>Check out <a href="https://aarongustafson.github.io/dynamic-datalist/demo/">the demo</a> for live examples (there are also <a href="https://aarongustafson.github.io/dynamic-datalist/demo/unpkg.html">unpkg</a> and <a href="https://aarongustafson.github.io/dynamic-datalist/demo/esm.html">ESM</a> builds if you want to test CDN delivery):</p><figure id="fig-2025-12-06-03" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/dynamic-datalist/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>The project is available on <a href="https://github.com/aarongustafson/dynamic-datalist">GitHub</a>. You can also install via npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/dynamic-datalist</code></pre><p>If you go that route, there are a few ways to register the element depending on your build setup:</p><h3 id="option-1%3A-define-it-yourself" tabindex="-1"><a class="header-anchor" href="#option-1%3A-define-it-yourself" aria-hidden="true">#</a> Option 1: Define it yourself</h3><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token punctuation">{</span> DynamicDatalistElement <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/dynamic-datalist”</span><span class="token punctuation">;</span>customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">“dynamic-datalist”</span><span class="token punctuation">,</span> DynamicDatalistElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="option-2%3A-let-the-helper-guard-registration" tabindex="-1"><a class="header-anchor" href="#option-2%3A-let-the-helper-guard-registration" aria-hidden="true">#</a> Option 2: Let the helper guard registration</h3><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/dynamic-datalist/define.js”</span><span class="token punctuation">;</span><span class="token comment">// or, when you need to wait:</span><span class="token keyword">import</span><span class="token punctuation">{</span> defineDynamicDatalist <span class="token punctuation">}</span><span class="token keyword">from</span><span class="token string">“@aarongustafson/dynamic-datalist/define.js”</span><span class="token punctuation">;</span><span class="token function">defineDynamicDatalist</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="option-3%3A-drop-the-helper-in-via-a-%3Cscript%3E-tag" tabindex="-1"><a class="header-anchor" href="#option-3%3A-drop-the-helper-in-via-a-%3Cscript%3E-tag" aria-hidden="true">#</a> Option 3: Drop the helper in via a <code>&lt;script&gt;</code> tag</h3><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>./node_modules/@aarongustafson/dynamic-datalist/define.js<span class="token punctuation">”</span></span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>module<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span></code></pre><p>Regardless of how you register it, there are no framework dependencies—just clean autocomplete powered by your API. As I mentioned, it’s also available via CDNs, such as unpkg too, if you’d prefer to go that route.</p>]]></content><amg:twitter><![CDATA[Want API-driven autocomplete suggestions in your forms? Here’s a web component that makes it happen.]]></amg:twitter><amg:summary><![CDATA[<p>HTML’s <code>datalist</code> element provides native autocomplete functionality, but it’s entirely static—you have to know all the options up front. The <code>dynamic-datalist</code> web component solves this by fetching suggestions from an API endpoint as users type, giving you the benefits of native autocomplete with the flexibility of dynamic data.</p>]]></amg:summary><summary type="html"><![CDATA[<p>HTML’s <code>datalist</code> element provides native autocomplete functionality, but it’s entirely static—you have to know all the options up front. The <code>dynamic-datalist</code> web component solves this by fetching suggestions from an API endpoint as users type, giving you the benefits of native autocomplete with the flexibility of dynamic data.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><category term="API" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/lazy-loading-images-based-on-screen-size/</id><title type="html"><![CDATA[Lazy Loading Images Based on Screen Size]]></title><link href="https://www.aaron-gustafson.com/notebook/lazy-loading-images-based-on-screen-size/" rel="alternate" type="text/html" /><published>2025-12-10T17:15:54Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Native lazy loading and <code>srcset</code> are great, but they have a limitation: they always load <em>some</em> variant of the image. The <code>lazy-img</code> web component takes a different approach—it can completely skip loading images when they don’t meet your criteria, whether that’s screen size, container size, or visibility in the viewport.</p><p>This is particularly valuable for mobile users on slow connections or limited data plans. If an image is only meaningful on larger screens, why waste their bandwidth loading it at all?</p><h2 id="the-performance-benefit" tabindex="-1"><a class="header-anchor" href="#the-performance-benefit" aria-hidden="true">#</a> The performance benefit</h2><p>Unlike <code>picture</code> or <code>srcset</code>, which always load some image variant, <code>lazy-img</code> can <strong>completely skip loading images</strong> on screens or containers below your specified threshold. Set <code>min-inline-size=&quot;768px&quot;</code> and mobile users will never download that image at all—saving data and speeding up page loads.</p><p>Once an image is loaded, however, it remains loaded even if the viewport or container is resized below the threshold. This is intentional—the component prevents unnecessary downloads but doesn’t unload images already in memory. You can control visibility with CSS if needed using the <code>loaded</code> and <code>qualifies</code> attributes (which we’ll get to shortly).</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>The <code>lazy-img</code> works pretty much identically to a regular <code>img</code> element, with all the attributes you know and love:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>A beautiful image<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>But that’s not very interesting. The real power comes from conditional loading.</p><h2 id="container-queries-(default)" tabindex="-1"><a class="header-anchor" href="#container-queries-(default)" aria-hidden="true">#</a> Container queries (default)</h2><p>Load an image only when its container reaches a minimum width:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>large-image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Large image<span class="token punctuation">”</span></span><span class="token attr-name">min-inline-size</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>500px<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>The image loads when the <code>lazy-img</code> element’s container reaches 500px wide. This is the default query mode—it uses <code>ResizeObserver</code> to watch the container size.</p><h2 id="media-queries" tabindex="-1"><a class="header-anchor" href="#media-queries" aria-hidden="true">#</a> Media queries</h2><p>You can lazy load images based on viewport width instead by switching to media query mode:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>desktop-image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Desktop image<span class="token punctuation">”</span></span><span class="token attr-name">min-inline-size</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>768px<span class="token punctuation">”</span></span><span class="token attr-name">query</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>media<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>With this configuration, the image loads when the browser window is at least 768px wide.</p><h2 id="view-mode-(scroll-based-loading)" tabindex="-1"><a class="header-anchor" href="#view-mode-(scroll-based-loading)" aria-hidden="true">#</a> View mode (scroll-based loading)</h2><p>Load images when they scroll into view using <code>IntersectionObserver</code> by switching to the “view” query type:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Loads when scrolled into view<span class="token punctuation">”</span></span><span class="token attr-name">query</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>view<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>The default behavior (<code>view-range-start=&quot;entry 0%&quot;</code>) loads as soon as any part of the image enters the viewport.</p><p>Control when images load with the <code>view-range-start</code> attribute:</p><p><strong>Load when 50% visible:</strong></p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Loads when half visible<span class="token punctuation">”</span></span><span class="token attr-name">query</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>view<span class="token punctuation">”</span></span><span class="token attr-name">view-range-start</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>entry 50%<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p><strong>Preload before entering viewport:</strong></p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Preloads 200px before visible<span class="token punctuation">”</span></span><span class="token attr-name">query</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>view<span class="token punctuation">”</span></span><span class="token attr-name">view-range-start</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>entry -200px<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>This creates a smooth user experience—images are already loaded by the time users scroll to them.</p><h2 id="responsive-images" tabindex="-1"><a class="header-anchor" href="#responsive-images" aria-hidden="true">#</a> Responsive images</h2><p>As with regular images, you can use <code>srcset</code> and <code>sizes</code> for responsive images:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image-800.jpg<span class="token punctuation">”</span></span><span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image-400.jpg 400w,image-800.jpg 800w,image-1200.jpg 1200w<span class="token punctuation">”</span></span><span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>(max-width: 600px) 400px,(max-width: 1000px) 800px,1200px<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Responsive image<span class="token punctuation">”</span></span><span class="token attr-name">min-inline-size</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>400px<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>The component waits until the conditions are met before loading a real image and the browser takes over from there.</p><h2 id="named-breakpoints" tabindex="-1"><a class="header-anchor" href="#named-breakpoints" aria-hidden="true">#</a> Named breakpoints</h2><p>You can also define named breakpoints using CSS custom properties:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token selector">:root</span><span class="token punctuation">{</span><span class="token property">–lazy-img-mq</span><span class="token punctuation">:</span> small<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token atrule"><span class="token rule">@media</span><span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 768px<span class="token punctuation">)</span></span><span class="token punctuation">{</span><span class="token selector">:root</span><span class="token punctuation">{</span><span class="token property">–lazy-img-mq</span><span class="token punctuation">:</span> medium<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token atrule"><span class="token rule">@media</span><span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 1024px<span class="token punctuation">)</span></span><span class="token punctuation">{</span><span class="token selector">:root</span><span class="token punctuation">{</span><span class="token property">–lazy-img-mq</span><span class="token punctuation">:</span> large<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span></code></pre><p>Then reference them in your markup:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Image with named breakpoints<span class="token punctuation">”</span></span><span class="token attr-name">named-breakpoints</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>medium, large<span class="token punctuation">”</span></span><span class="token attr-name">query</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>media<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>The image loads when <code>–lazy-img-mq</code> matches “medium” or “large”.</p><h2 id="preventing-layout-shift" tabindex="-1"><a class="header-anchor" href="#preventing-layout-shift" aria-hidden="true">#</a> Preventing layout shift</h2><p>As with regular images, don’t forget to use <code>width</code> and <code>height</code> attributes to prevent Cumulative Layout Shift (CLS):</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lazy-img</span><span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>image.jpg<span class="token punctuation">”</span></span><span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>A beautiful image<span class="token punctuation">”</span></span><span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>800<span class="token punctuation">”</span></span><span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>600<span class="token punctuation">”</span></span><span class="token attr-name">min-inline-size</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>768px<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lazy-img</span><span class="token punctuation">&gt;</span></span></code></pre><p>The browser reserves the correct space while the image loads, preventing content from jumping around.</p><h2 id="state-attributes-for-styling" tabindex="-1"><a class="header-anchor" href="#state-attributes-for-styling" aria-hidden="true">#</a> State attributes for styling</h2><p>The component provides <code>loaded</code> and <code>qualifies</code> attributes you can use in CSS:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token comment">/* Hide images that loaded but no longer meet conditions <em>/</span><span class="token selector">lazy-img[loaded]:not([qualifies])</span><span class="token punctuation">{</span><span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">/</em> Show a placeholder for images that qualify but haven’t loaded */</span><span class="token selector">lazy-img[qualifies]:not([loaded])::before</span><span class="token punctuation">{</span><span class="token property">content</span><span class="token punctuation">:</span><span class="token string">“Loading…”</span><span class="token punctuation">;</span><span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span><span class="token property">padding</span><span class="token punctuation">:</span> 2em<span class="token punctuation">;</span><span class="token property">background</span><span class="token punctuation">:</span> #f0f0f0<span class="token punctuation">;</span><span class="token property">text-align</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><h2 id="events" tabindex="-1"><a class="header-anchor" href="#events" aria-hidden="true">#</a> Events</h2><p>If you crave control, you can add your own functionality by listening for when images load:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> lazyImg <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">“lazy-img”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>lazyImg<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“lazy-img:loaded”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Image loaded:”</span><span class="token punctuation">,</span> event<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>src<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="performance" tabindex="-1"><a class="header-anchor" href="#performance" aria-hidden="true">#</a> Performance</h2><p>The component is highly optimized:</p><ul><li><strong>Throttled resize</strong>: Resize events are throttled to prevent excessive checks</li><li><strong>Shared <code>ResizeObserver</code></strong>: Multiple images observing the same container share a single ResizeObserver</li><li><strong>Shared window resize listener</strong>: Media query mode shares a single window resize listener</li><li><strong>Shared <code>IntersectionObserver</code></strong>: View mode with the same <code>view-range-start</code> shares an <code>IntersectionObserver</code></li><li><strong>Clean disconnection</strong>: Properly cleans up observers when elements are removed</li></ul><p>Even with hundreds of <code>lazy-img</code> elements on a page, performance remains excellent.</p><h2 id="progressive-enhancement" tabindex="-1"><a class="header-anchor" href="#progressive-enhancement" aria-hidden="true">#</a> Progressive enhancement</h2><p>If JavaScript fails to load, images simply don’t appear (unless using immediate loading mode). This might sound problematic, but for non-critical images—decorative graphics, supplementary screenshots, marketing imagery—it’s often exactly what you want. Your content remains accessible; you just lose the enhancements.</p><p>For critical images that are part of your content, use standard <code>img</code> tags. Use <code>lazy-img</code> for conditional enhancements.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>Explore <a href="https://aarongustafson.github.io/lazy-img/demo/">the demo</a> to see container queries, media queries, scroll-based loading, and more in action:</p><figure id="fig-2025-12-06-04" class="media-container"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/lazy-img/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>Check out the project on <a href="https://github.com/aarongustafson/lazy-img">GitHub</a>. Install via npm:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/lazy-img</code></pre><p>Import and use:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">“@aarongustafson/lazy-img”</span><span class="token punctuation">;</span></code></pre><p>Based on my original <a href="https://github.com/easy-designs/easy-lazy-images.js">Easy Lazy Images</a> concept, reimagined as a modern custom element.</p>]]></content><amg:twitter><![CDATA[Want to skip loading images entirely on mobile? Here’s a web component that does just that.]]></amg:twitter><amg:summary><![CDATA[<p>Native lazy loading and <code>srcset</code> are great, but they have a limitation: they always load <em>some</em> variant of the image. The <code>lazy-img</code> web component takes a different approach—it can completely skip loading images when they don’t meet your criteria, whether that’s screen size, container size, or visibility in the viewport.</p><p>This is particularly valuable for mobile users on slow connections or limited data plans. If an image is only meaningful on larger screens, why waste their bandwidth loading it at all?</p>]]></amg:summary><summary type="html"><![CDATA[<p>Native lazy loading and <code>srcset</code> are great, but they have a limitation: they always load <em>some</em> variant of the image. The <code>lazy-img</code> web component takes a different approach—it can completely skip loading images when they don’t meet your criteria, whether that’s screen size, container size, or visibility in the viewport.</p><p>This is particularly valuable for mobile users on slow connections or limited data plans. If an image is only meaningful on larger screens, why waste their bandwidth loading it at all?</p>]]></summary><category term="web components" /><category term="progressive enhancement" /><category term="HTML" /><category term="performance" /><category term="images" /><category term="responsive design" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-for-obfuscating-form-fields/</id><title type="html"><![CDATA[A Web Component for Obfuscating Form Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-for-obfuscating-form-fields/" rel="alternate" type="text/html" /><published>2025-12-06T20:03:47Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>We have the password reveal pattern for passwords, but what about other sensitive fields that need to be readable while editing and obfuscated while at rest? The <code>form-obfuscator</code> web component does exactly that.</p><h2 id="basic-usage" tabindex="-1"><a class="header-anchor" href="#basic-usage" aria-hidden="true">#</a> Basic usage</h2><p>Wrap any text field in the component and it will automatically obfuscate the value when the field loses focus:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>secret-key-1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>What was your first pet’s name?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>secret-key-1<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>secret-key-1<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>When users click into the field, they see the actual value. When they click away, it’s replaced with asterisks (<em>). The real value is preserved in a hidden field for form submission.</p><h2 id="custom-obfuscation-characters" tabindex="-1"><a class="header-anchor" href="#custom-obfuscation-characters" aria-hidden="true">#</a> Custom obfuscation characters</h2><p>If you don’t like asterisks, you can specify any character you like:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">character</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>•<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>account<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Account Number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>account<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>account<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>Or get creative:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">character</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>🤐<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Social Security Number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><h2 id="pattern-based-obfuscation" tabindex="-1"><a class="header-anchor" href="#pattern-based-obfuscation" aria-hidden="true">#</a> Pattern-based obfuscation</h2><p>Sometimes you want to show part of the value while hiding the rest. The <code>pattern</code> attribute lets you specify which characters to keep visible:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>\d{4}$<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Social Security Number<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>ssn<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This keeps the last four digits visible while replacing everything else with your obfuscation character. Perfect for Social Security Numbers, credit cards, or phone numbers where showing the last few digits helps users confirm they’ve entered the right value.</p><h2 id="limiting-displayed-characters" tabindex="-1"><a class="header-anchor" href="#limiting-displayed-characters" aria-hidden="true">#</a> Limiting displayed characters</h2><p>Use the <code>maxlength</code> attribute to cap how many characters appear when obfuscated:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">maxlength</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>4<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Password<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>text<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>password<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>Even if the user enters a 20-character value, only four asterisks will be displayed when the field is obfuscated. This prevents giving away information about the length of the information entered.</p><h2 id="custom-replacement-functions" tabindex="-1"><a class="header-anchor" href="#custom-replacement-functions" aria-hidden="true">#</a> Custom replacement functions</h2><p>For complete control, you can provide a JavaScript function via the <code>replacer</code> attribute:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript">window<span class="token punctuation">.</span><span class="token function-variable function">emailReplacer</span><span class="token operator">=</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> username <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">var</span> domain <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">return</span> username<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">.</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span><span class="token string">&quot;</em>“</span><span class="token punctuation">)</span><span class="token operator">+</span> domain<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>^(.<em>?)(@.+)$<span class="token punctuation">“</span></span><span class="token attr-name">replacer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>return emailReplacer(arguments)<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Email Address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>text<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email<span class="token punctuation">“</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span><a href="mailto:user@example.com">user@example.com</a><span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This example uses a pattern to separate the username from the domain, then obfuscates only the username portion, leaving <code>@example.com</code> visible.</p><p>Here’s another practical example for credit cards:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript"><span class="token keyword">function</span><span class="token function">cardNumberReplacer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> beginning <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">var</span> final_digits <span class="token operator">=</span> arguments<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token keyword">return</span> beginning<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\d</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span><span class="token string">”</em>“</span><span class="token punctuation">)</span><span class="token operator">+</span> final_digits<span class="token punctuation">;</span><span class="token punctuation">}</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>^((?:[\d]+-)+)(\d+)$<span class="token punctuation">“</span></span><span class="token attr-name">replacer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>return cardNumberReplacer(arguments)<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>cc<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Credit Card<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>text<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>cc<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>cc<span class="token punctuation">“</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>1234-5678-9012-3456<span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This displays as <code><strong><strong>-</strong></strong>-****-3456</code>, showing only the last group of digits.</p><h2 id="combining-attributes" tabindex="-1"><a class="header-anchor" href="#combining-attributes" aria-hidden="true">#</a> Combining attributes</h2><p>You can combine these attributes for sophisticated obfuscation patterns:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-obfuscator</span><span class="token attr-name">pattern</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>\d{4}$<span class="token punctuation">“</span></span><span class="token attr-name">character</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>•<span class="token punctuation">“</span></span><span class="token attr-name">maxlength</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>16<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>card<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span>Credit Card<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>text<span class="token punctuation">“</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>card<span class="token punctuation">“</span></span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>card<span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-obfuscator</span><span class="token punctuation">&gt;</span></span></code></pre><p>This keeps the last 4 digits visible, uses bullets for obfuscation, and limits the display to 16 characters total.</p><h2 id="event-handling" tabindex="-1"><a class="header-anchor" href="#event-handling" aria-hidden="true">#</a> Event handling</h2><p>The component dispatches custom events when values are hidden or revealed:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> obfuscator <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>obfuscator<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator:hide”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Field obfuscated:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>field<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>obfuscator<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“form-obfuscator:reveal”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">“Field revealed:”</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>detail<span class="token punctuation">.</span>field<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>You can access both the visible field and the hidden field through <code>event.detail.field</code> and <code>event.detail.hidden</code> respectively.</p><h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works" aria-hidden="true">#</a> How it works</h2><p>The component creates a hidden <code>input</code> field to store the actual value for form submission. When the visible field loses focus, it:</p><ol><li>Copies the current value to the hidden field</li><li>Applies your obfuscation rules to create the display value</li><li>Updates the visible field with the obfuscated value</li><li>Dispatches the <code>form-obfuscator:hide</code> event</li></ol><p>When the field gains focus, it:</p><ol><li>Restores the real value from the hidden field</li><li>Updates the visible field</li><li>Dispatches the <code>form-obfuscator:reveal</code> event</li></ol><p>The source order ensures the hidden field is the one that gets submitted with the form.</p><h2 id="progressive-enhancement" tabindex="-1"><a class="header-anchor" href="#progressive-enhancement" aria-hidden="true">#</a> Progressive enhancement</h2><p>The component makes no assumptions about your markup—it works with any text-style <code>input</code> element. If JavaScript fails to load, the field behaves like a normal <code>input</code>, which is exactly what you want. Users can still enter and submit values; they just won’t get the obfuscation behavior.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I’ve created <a href="https://aarongustafson.github.io/form-obfuscator/demo/">a comprehensive demo page showing the various configuration options</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-obfuscator/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>Check out the full project over on <a href="https://github.com/aarongustafson/form-obfuscator">GitHub</a> or install via <code>npm</code>:</p><pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span><span class="token function">install</span> @aarongustafson/form-obfuscator</code></pre><p>Import and use:</p><pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span><span class="token string">”@aarongustafson/form-obfuscator&quot;</span><span class="token punctuation">;</span></code></pre><p>No dependencies, just a straightforward way to add field obfuscation to your forms.</p>]]></content><amg:twitter><![CDATA[Need to obfuscate form field values when they’re not being edited? Here’s a web component for that.]]></amg:twitter><amg:summary><![CDATA[<p>We have the password reveal pattern for passwords, but what about other sensitive fields that need to be readable while editing and obfuscated while at rest? The <code>form-obfuscator</code> web component does exactly that.</p>]]></amg:summary><summary type="html"><![CDATA[<p>We have the password reveal pattern for passwords, but what about other sensitive fields that need to be readable while editing and obfuscated while at rest? The <code>form-obfuscator</code> web component does exactly that.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /><category term="security" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/optimizing-your-codebase-for-ai-coding-agents/</id><title type="html"><![CDATA[Optimizing Your Codebase for AI Coding Agents]]></title><link href="https://www.aaron-gustafson.com/notebook/optimizing-your-codebase-for-ai-coding-agents/" rel="alternate" type="text/html" /><published>2025-10-21T22:11:32Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I’ve been playing around a bit with GitHub Copilot as an autonomous agent to help with software development. The results have been mixed, but positive overall. I made an interesting discovery when I took the time to read through the agent’s reasoning over a particular task. I thought the task was straightforward, but I was wrong. Watching the agent work was like watching someone try to navigate an unfamiliar room, in complete darkness, with furniture and Lego bricks scattered everywhere.</p><p>The good news? Most of the issues weren’t actually <em>code</em> problems; they were organizational and documentation problems. The kinds of problems that make tasks hard for humans too.</p><p>As I watched the agent struggle, I realized that optimizing for AI agents is really just about removing ambiguity and making implicit knowledge explicit. In other words: it’s just good engineering.</p><h2 id="what-did-i-learn%3F" tabindex="-1"><a class="header-anchor" href="#what-did-i-learn%3F" aria-hidden="true">#</a> What did I learn?</h2><p>After reviewing the agent’s execution logs (which read like a stream-of-consciousness diary of confusion), several patterns emerged:</p><h3 id="1.-documentation-sprawl-is-an-efficiency-killer" tabindex="-1"><a class="header-anchor" href="#1.-documentation-sprawl-is-an-efficiency-killer" aria-hidden="true">#</a> 1. <strong>Documentation sprawl is an efficiency killer</strong></h3><p>The agent spent roughly 40% of its time just trying to figure out which documentation to trust. We had instructions in workflow comments, the README, task specific instructions, and more. In other words, we had no clear source of truth. Pieces of that truth were scattered across multiple files and the docs were inconsistent and — in some cases — contradictory.</p><p>Sound familiar? It’s the equivalent of having five different “getting started” guides that all got written at different times by different people and nobody bothered to consolidate them. (If you’ve ever worked on a project that’s been around for more than a year with no one in charge of documentation, you know exactly what I’m talking about.)</p><p><strong>The fix:</strong> Establish a single source of truth. Ruthlessly. We consolidated everything into one comprehensive guide and updated all references to point <em>only</em> there. Deprecated docs were deleted and/or redirected, as appropriate. No more choose your own adventure. No more guessing.</p><h3 id="2.-agents-won%E2%80%99t-optimize-themselves" tabindex="-1"><a class="header-anchor" href="#2.-agents-won%E2%80%99t-optimize-themselves" aria-hidden="true">#</a> 2. <strong>Agents won’t optimize themselves</strong></h3><p>Here’s a fun one: the agent ran several full production builds—complete with image processing, template compilation… the whole shebang—just to validate a markdown file was in the right format. These builds took 30-60 seconds. <em>Each time.</em></p><p>This is like requiring someone to assemble an entire car just to check if the owner’s guide is displaying the right “check engine” symbol. Technically it works, but yikes.</p><p><strong>The fix:</strong> Write fast, focused validation scripts. One tool for each job. Tell the agent what the utility is, where to find it, and how to use it. Give it explicit instructions to use utility scripts in lieu of full site builds whenever possible.</p><h3 id="3.-ambiguity-breeds-confusion-(and-wasted-tokens)" tabindex="-1"><a class="header-anchor" href="#3.-ambiguity-breeds-confusion-(and-wasted-tokens)" aria-hidden="true">#</a> 3. <strong>Ambiguity breeds confusion (and wasted tokens)</strong></h3><p>The agent spent 15+ minutes having an internal philosophical debate about whether to process test data. Should it reject it? Accept it? Create a placeholder? The instructions didn’t say, so the agent did what any of us would do: it agonized — or at least feigned agonizing — over the decision and tried to infer intent from context clues.</p><p><strong>The fix:</strong> Be explicit about edge cases. We added a dedicated section for handling test form submissions. No more guessing.</p><h2 id="there%E2%80%99s-a-pattern-here" tabindex="-1"><a class="header-anchor" href="#there%E2%80%99s-a-pattern-here" aria-hidden="true">#</a> There’s a pattern here</h2><p>If you squint, all of these issues share a common root cause: <strong>implicit assumptions</strong>.</p><p>We assumed humans would know to check one doc instead of five. We assumed the difference between validation and building was obvious. We assumed everyone would understand how to handle edge cases.</p><p>AI agents don‘t — can’t? — make those assumptions. They need explicit instructions, clear boundaries, and unambiguous inputs. Honestly? So do humans. We’re just better at muddling through — or think we are.</p><h2 id="early-results" tabindex="-1"><a class="header-anchor" href="#early-results" aria-hidden="true">#</a> Early results</h2><p>After implementing these changes, we expect (and early testing confirms):</p><ul><li>~40% reduction in processing time,</li><li>~75% reduction in token usage, and</li><li>&gt;80% reduction in confusion and circular reasoning.</li></ul><p>But here’s the thing: these improvements don’t just help the AI agent. They help <em>everyone</em>. The consolidated documentation is easier to navigate. The fast validation scripts are useful for humans too. The explicit edge case handling prevents future questions.</p><h2 id="the-key-to-reducing-toil%3A-excellent-docs-and-tools" tabindex="-1"><a class="header-anchor" href="#the-key-to-reducing-toil%3A-excellent-docs-and-tools" aria-hidden="true">#</a> The key to reducing toil: excellent docs and tools</h2><p>Optimizing for AI agents isn’t really about AI. It’s about removing ambiguity, eliminating redundancy, and making implicit knowledge explicit. It’s about writing code and documentation that doesn’t require a deep understanding of the project to comprehend.</p><p>In other words: it’s just good engineering.</p><p>So if you’re working with AI coding agents — or planning to — invest in your docs and tooling. Don’t think of it as wasted time “writing for robots.” Think of it as paying down documentation debt and building an efficient engineering process.</p><p>Your future self, your teammates, and the bots will thank you.</p><h2 id="afterword" tabindex="-1"><a class="header-anchor" href="#afterword" aria-hidden="true">#</a> Afterword</h2><p>Interestingly, an AI agent was a particularly useful partner in finding and addressing the technical debt we’d been living with. Sometimes you need a pedantic robot to point out that your house is a mess.</p>]]></content><amg:summary><![CDATA[<p>I’ve been playing around a bit with GitHub Copilot as an autonomous agent to help with software development. The results have been mixed, but positive overall. I made an interesting discovery when I took the time to read through the agent’s reasoning over a particular task. I thought the task was straightforward, but I was wrong. Watching the agent work was like watching someone try to navigate an unfamiliar room, in complete darkness, with furniture and Lego bricks scattered everywhere.</p>]]></amg:summary><summary type="html"><![CDATA[<p>I’ve been playing around a bit with GitHub Copilot as an autonomous agent to help with software development. The results have been mixed, but positive overall. I made an interesting discovery when I took the time to read through the agent’s reasoning over a particular task. I thought the task was straightforward, but I was wrong. Watching the agent work was like watching someone try to navigate an unfamiliar room, in complete darkness, with furniture and Lego bricks scattered everywhere.</p>]]></summary><category term="AI/ML" /><category term="software development" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/posts/2025-10-21/hero.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-for-conditionally-displaying-fields/</id><title type="html"><![CDATA[A Web Component for Conditionally Displaying Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-for-conditionally-displaying-fields/" rel="alternate" type="text/html" /><published>2025-10-20T17:05:50Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Building on my recent work in the <a href="https://www.aaron-gustafson.com/notebook/series/forms/">form utility space</a>, I’ve created a new web component that allows you to conditionally display form fields based on the values of other fields: <code>form-show-if</code>.</p><p>This component tackles a common UX pattern that HTML doesn’t natively support. You know the scenario—you have a form where certain fields should only appear when specific conditions are met. Maybe you want to show shipping address fields only when someone checks “Ship to different address,” or display a text input for “Other” when someone selects that option from a dropdown. This web component makes that setup effortless — and declarative.</p><p>You set up <code>form-show-if</code> like this:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-show-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>contact_method=phone<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone Number<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>tel<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>phone<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>phone<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-show-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>You wrap any field and its <code>label</code> in the component and then declare the conditions under which it should be displayed in the <code>conditions</code> attribute.</p><h2 id="defining-the-display-conditions" tabindex="-1"><a class="header-anchor" href="#defining-the-display-conditions" aria-hidden="true">#</a> Defining the display conditions</h2><p>Each condition is a key/value pair where the key aligns to the <code>name</code> of the field you need to observe and the value is the value that triggers the display. If any value should trigger the display, use an asterisk (<code>*</code>) as the value. In the example above, the field will become visible only if — in a theoretical contact method choice — a user chooses “phone” as the method they want used.</p><p>The <code>conditions</code> attribute can be populated with as many dependencies as you need. Multiple conditions are separated by double vertical pipes (<code>||</code>), as in this example:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-show-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>contact_method=phone||contact_method=text_message<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>phone-number<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Phone Number<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>tel<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>phone<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>phone<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-show-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>Here the field depends on one of the following conditions being true:</p><ol><li>the field matching <code>[name=&quot;contact_method&quot;]</code> has a value of “phone” <em>or</em></li><li>the field matching <code>[name=&quot;contact_method&quot;]</code> has a value of “text_message”</li></ol><p>If the field you reference doesn’t exist, no errors will be thrown—it will just quietly exit.</p><h2 id="customizing-the-show%2Fhide-behavior" tabindex="-1"><a class="header-anchor" href="#customizing-the-show%2Fhide-behavior" aria-hidden="true">#</a> Customizing the show/hide behavior</h2><p>By default, the component uses the <code>hidden</code> attribute to hide the wrapped content when it’s not needed. But you can customize this behavior using CSS classes instead:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-show-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>shipping-method=express<span class="token punctuation">”</span></span><span class="token attr-name">disabled-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>fade-out<span class="token punctuation">”</span></span><span class="token attr-name">enabled-class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>fade-in<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>delivery-date<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Express Delivery Date<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>date<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>delivery-date<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>delivery-date<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-show-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>When using custom classes:</p><ul><li><strong><code>disabled-class</code></strong> is applied when the condition is not met (field should be hidden)</li><li><strong><code>enabled-class</code></strong> is applied when the condition is met (field should be shown)</li></ul><p>Both are optional. Just remember that if you define a <code>disabled-class</code>, the <code>hidden</code> attribute will not be used — you will need to accessibly hide the content yourself.</p><p>This gives you complete control over the visual presentation. You could use CSS transitions for smooth animations, apply different styling states, or integrate with your existing design system’s utility classes.</p><h2 id="handling-form-state-properly" tabindex="-1"><a class="header-anchor" href="#handling-form-state-properly" aria-hidden="true">#</a> Handling form state properly</h2><p>The component doesn’t just toggle visibility—it also manages the form state correctly. When fields are hidden, they’re automatically disabled using the <code>disabled</code> attribute. If there are any sibling fields in the component, they will be disabled as well. This prevents these fields from being submitted with the form and ensures they don’t interfere with form validation.</p><p>When conditions are met and fields become visible, they’re re-enabled automatically. This behavior works seamlessly with both native form validation and custom validation scripts.</p><h2 id="real-world-examples" tabindex="-1"><a class="header-anchor" href="#real-world-examples" aria-hidden="true">#</a> Real-world examples</h2><p>Here are some practical use cases where this component shines:</p><p><strong>“Other” option handling:</strong></p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>How did you hear about us?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>radio<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>source<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>google<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span> Google<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span><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>radio<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>source<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>friend<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span> Friend<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span><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>radio<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>source<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>other<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span> Other<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-show-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>source=other<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>source-other<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Please specify<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>source-other<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>source-other<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-show-if</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span></code></pre><p><strong>Specific value matching:</strong></p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-show-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span><a href="mailto:email=test@example.com">email=test@example.com</a><span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>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>debug-info<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Debug Information<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>textarea</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>debug-info<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>debug-info<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>textarea</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>small</span><span class="token punctuation">&gt;</span></span>This field only shows for test accounts<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>small</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-show-if</span><span class="token punctuation">&gt;</span></span></code></pre><h2 id="progressive-enhancement-in-action" tabindex="-1"><a class="header-anchor" href="#progressive-enhancement-in-action" aria-hidden="true">#</a> Progressive enhancement in action</h2><p>Like all good web components, <code>form-show-if</code> follows progressive enhancement principles. If JavaScript fails to load or the browser doesn’t support custom elements, your form still works—users just see all the fields all the time. Not ideal for the user experience, but nothing breaks either.</p><p>The component is lightweight, has no dependencies, and works in all modern browsers.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I’ve put together <a href="https://aarongustafson.github.io/form-show-if/demo/">a comprehensive demo showing various use cases and configurations</a> over on GitHub.</p><p>The demo includes examples of:</p><ul><li>Basic show/hide functionality</li><li>Multiple condition logic</li><li>Custom CSS class integration</li><li>Complex form scenarios with radio buttons and checkboxes</li><li>Different field grouping approaches</li></ul><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-show-if/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab it</h2><p>You can view the entire project (and suggest enhancements) over on <a href="https://github.com/aarongustafson/form-show-if">the form-show-if component’s GitHub repo</a>. The component is available as both a standard script and an ES module, so you can integrate it however works best for your project.</p><p>Installation is straightforward—just include the script in your page and start using the <code>form-show-if</code> element. No build step required, no framework dependencies, just clean, standards-based progressive enhancement.</p>]]></content><amg:twitter><![CDATA[Sometimes you only want a field to show when certain other fields have a (particular) value. The `form-show-if` web component enables that.]]></amg:twitter><amg:summary><![CDATA[<p>Building on my recent work in the <a href="https://www.aaron-gustafson.com/notebook/series/forms/">form utility space</a>, I’ve created a new web component that allows you to conditionally display form fields based on the values of other fields: <code>form-show-if</code>.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Building on my recent work in the <a href="https://www.aaron-gustafson.com/notebook/series/forms/">form utility space</a>, I’ve created a new web component that allows you to conditionally display form fields based on the values of other fields: <code>form-show-if</code>.</p>]]></summary><category term="web components" /><category term="web forms" /><category term="progressive enhancement" /><category term="HTML" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/identifying-accessibility-data-gaps-in-codegen-models/</id><title type="html"><![CDATA[Identifying Accessibility Data Gaps in CodeGen Models]]></title><link href="https://www.aaron-gustafson.com/notebook/identifying-accessibility-data-gaps-in-codegen-models/" rel="alternate" type="text/html" /><published>2025-10-16T19:12:02Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Late last year, I probed an LLM’s responses to HTML code generation prompts to assess its adherence to accessibility best practices. The results were unsurprisingly disappointing — roughly what I’d expect from a developer aware of accessibility but unsure how to implement it. The study highlighted key areas where training data needs improvement.</p><h2 id="why-take-on-this-challenge%3F" tabindex="-1"><a class="header-anchor" href="#why-take-on-this-challenge%3F" aria-hidden="true">#</a> Why take on this challenge?</h2><p>I get it — you probably rolled your eyes at yet another “AI and accessibility” post. Maybe you think AI-assisted coding is overhyped, environmentally harmful, unreliable, or just plain dangerous for our craft. I share many of those concerns. But here’s the thing: whether we like it or not, codegen models aren’t going anywhere. GitHub Copilot has millions of users, and tools like Claude Code and Cursor are rapidly gaining popularity.</p><p>So we have a choice: we can complain about the inevitable tide of AI-generated garbage code, or we can get in there and figure out how to make it better — especially when it comes to accessibility.</p><p>We’re facing a looming wave of inaccessible code that will be extremely difficult to remediate later. The foundation models are already being trained on the collective output of the web’s development community — a community that doesn’t have a high bar high for accessibility already. Codegen models are a massive consultancy staffed with <a href="https://christianheilmann.com/2015/07/17/the-full-stackoverflow-developer/">full StackOverflow developers</a>. We need to figure out how to make them part of the solution, not part of the problem.</p><p>It’s also worth noting that the better we make the output of these models, the fewer bugs will be generated. That, in turn, means fewer accessibility issues to fix later. If we don’t, there are plenty of AI-assisted scanners out there happy to burn the rainforest to find and remediate the bugs after the fact. We risk doubling the environmental impact—once to generate the bug, and again to fix it. That’s not the future I want. The reality here is that the only way to deal with this flood of AI-generated code is to make sure it’s good code in the first place.</p><h2 id="how-did-i-conduct-my-research%3F" tabindex="-1"><a class="header-anchor" href="#how-did-i-conduct-my-research%3F" aria-hidden="true">#</a> How did I conduct my research?</h2><p>Rather than relying on anecdotal evidence or cherry-picked examples, I built a systematic approach to evaluate how well LLMs — starting with GPT-4 — generate accessible HTML. The methodology is straightforward but comprehensive: I created a Python testing framework that sent carefully crafted prompts to Azure OpenAI’s GPT 4 model, collected the generated HTML responses, and then manually analyzed these responses for accessibility compliance.</p><p>Here’s how it works:</p><p><strong>Prompt Engineering</strong>: I designed prompts that ask for specific UI components—form fields, navigation menus, interactive elements—without explicitly mentioning accessibility requirements. This gives us a baseline of what the model considers “standard” output. I included one prompt that specifically requested accessibility features to see if the model could improve when guided. I suspected it would often add ARIA attributes without addressing underlying issues, but I wanted to validate that too.</p><p><strong>Response Collection</strong>: For each prompt, I generated 10 iterations at high temperature (0.95) to capture the model’s range of responses. Each unique response got saved as an individual HTML file for analysis.</p><p><strong>Systematic Analysis</strong>: I manually review each generated code snippet, cataloging accessibility errors, warnings, and missed opportunities. I tried using the LLM as a judge, but even with a detailed rubric, the results were poor. My eval looked specifically for things like:</p><ul><li>Improper semantic HTML usage</li><li>Missing or incorrect ARIA attributes</li><li>Keyboard navigation issues</li><li>Screen reader compatibility problems</li><li>Form labeling errors</li></ul><p>When I identified errors, I remediated them and committed the remediated file to the repo with a commit message that included all of the issues and warnings on its own line.</p><p><strong>Diff-Based Retesting</strong>: I wanted to see if diff data could improve future codegen requests, so I created a tool to generate a collection of <code>.diff</code> files for each pattern that included the commit message as a header in each file. I then used those diff files as part of a new instance of the prompt to test whether the model can improve its output when guided.</p><h2 id="what-did-i-learn%3F" tabindex="-1"><a class="header-anchor" href="#what-did-i-learn%3F" aria-hidden="true">#</a> What did I learn?</h2><p>After analyzing hundreds of generated code snippets, the results are sobering. The model consistently demonstrates what I’d describe as superficial awareness without true understanding — it knows accessibility concepts exist but fundamentally misunderstands their purpose and proper implementation.</p><p>Here are some of the patterns I’ve documented:</p><p><strong>Form Label Disasters</strong>: When asked to create a required text field, the model failed to include a visible label:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>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>orangeColor<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>orangeColor<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>What color is an orange?<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span></code></pre><p>Sure, the <code>placeholder</code> attribute is there, and in a pinch it will be included in a field’s accessible name calculation, but sighted users will lose the label as soon as they start typing.</p><p><strong>ARIA Attribute Confusion</strong>: The model would routinely involve ARIA for no reason:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>color-question<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>What color is an orange? <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span><span class="token value css language-css"><span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span></span><span class="token punctuation">”</span></span></span><span class="token punctuation">&gt;</span></span>*<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>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>color-question<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>color-question<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token attr-name">aria-required</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>true<span class="token punctuation">”</span></span><span class="token attr-name">aria-labelledby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>color-question<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span></code></pre><p>Here the <code>for</code> attribute already establishes the relationship between the label and input, so <code>aria-labelledby</code> is redundant. A bit of a nitpick, but the <code>aria-required=&quot;true&quot;</code> is also unnecessary since the native <code>required</code> attribute already conveys that information to assistive technologies. <code>aria-required=&quot;true&quot;</code> is only needed when creating custom form controls non-semantic markup.</p><p><strong>Redundant ARIA</strong>: Keeping on the ARIA redundancy, consider examples like this:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>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>radio<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>option1<span class="token punctuation">”</span></span><span class="token attr-name">aria-labelledby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>label1<span class="token punctuation">”</span></span><span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>Option 1<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>option1<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>label1<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Option 1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span></code></pre><p>This redundancy raises the question <em>why‽</em></p><p><strong>Required Field Misapplication</strong>: For checkbox groups where users need to select “one or more,” the model often adds <code>required</code> to individual checkboxes:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">&gt;</span></span>What fruits do you like?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>checkbox<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>bananas<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>fruits<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>bananas<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>bananas<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Bananas<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>checkbox<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>oranges<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>fruits<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>oranges<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>oranges<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Oranges<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>checkbox<span class="token punctuation">”</span></span><span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>apples<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>fruits<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>apples<span class="token punctuation">”</span></span><span class="token attr-name">required</span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>apples<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Apples<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span><span class="token value css language-css"><span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span></span><span class="token punctuation">”</span></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>validation-error<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>You must choose one or more fruits<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">&gt;</span></span></code></pre><p>This breaks the intended behavior—if any checkbox is marked required, it must be checked for form validation to pass. For a web component that addresses this limitation in HTML, see my post “<a href="/notebook/requirement-rules-for-checkboxes/">Requirement Rules for Checkboxes</a>.”</p><p><strong>Grouped Field Confusion</strong>: Not understanding when to use <code>fieldset</code> and <code>legend</code> (or at least using <code>role=&quot;group&quot;</code> and <code>aria-labelledby</code>) on a field group:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Select Theme:<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>radio<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>light<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>theme<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>light<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>light<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Light<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>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>radio<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>dark<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>theme<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>dark<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>dark<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Dark<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>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>radio<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>high-contrast<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>theme<span class="token punctuation">”</span></span><span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>high-contrast<span class="token punctuation">”</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>high-contrast<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>High Contrast<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">&gt;</span></span>You can change this later<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span></code></pre><p>Ideally, this would be a <code>fieldset</code> with a <code>legend</code> and the descriptive text would appear right after the <code>legend</code> and be associated with the group using <code>aria-describedby</code>.</p><p><strong>Color-Only Error Indication</strong>: Generating error states that rely solely on color changes without text indicators or proper ARIA attributes to convey the error state to screen readers.</p><p><strong>Unnecessary Role Additions</strong>: Adding redundant roles like <code>role=&quot;radiogroup&quot;</code> to properly structured fieldsets containing radio inputs, where the native semantics already provide the correct accessibility tree.</p><p><strong>Missing Error State Management</strong>: Failing to include <code>aria-invalid=&quot;true&quot;</code> on fields with errors or properly associate error messages with their corresponding form controls.</p><p><strong>Lack of Wayfinding Help</strong>: Failing to include navigational labels and <code>aria-current=&quot;page&quot;</code> in a breadcrumb nav.</p><p><strong>Adding Unnecessary JavaScript</strong>: Even though it was instructed to only generate JavaScript when absolutely necessary, the model would often inject JavaScript for simple tasks that could be handled with HTML and CSS alone.</p><h2 id="how-does-this-help%3F" tabindex="-1"><a class="header-anchor" href="#how-does-this-help%3F" aria-hidden="true">#</a> How Does This Help?</h2><p>Here’s where things get interesting — and hopeful. When I retested using prompts that included accessibility hints, the model’s output improved dramatically. Not just slightly better, but often going from fundamentally broken to genuinely accessible.</p><p>For example, when I added diff data related to fieldset use to a prompt about radio button groups, the model switched from generating meaningless <code>div</code> wrappers to proper semantic structures.</p><p>This suggests the model can produce quality code if properly primed. It also indicates that the training data likely lacks sufficient examples of well-implemented accessible components. If the model had been trained on a richer dataset of accessible code, it might not need such explicit guidance to produce good results.</p><h2 id="where-do-we-go-from-here%3F" tabindex="-1"><a class="header-anchor" href="#where-do-we-go-from-here%3F" aria-hidden="true">#</a> Where Do We Go From Here?</h2><p>These findings point to several concrete approaches for improving accessibility in AI-generated code:</p><p><strong>Enhanced Training Data</strong>: The models need exposure to more high-quality, accessible code examples. Current training data clearly overrepresents inaccessible implementations. We need comprehensive datasets of properly implemented accessible components across different frameworks and use cases.</p><p><strong>Accessibility-Aware Fine-Tuning</strong>: Post-training refinement specifically focused on accessibility compliance could help models prioritize inclusive patterns. This could involve training on accessibility-annotated code pairs — showing inaccessible implementations alongside their accessible counterparts, like the diffs do.</p><p><strong>Prompt Engineering Guidelines</strong>: Tool creators should integrate accessibility considerations into their default system prompts. Instead of just asking for “clean, semantic HTML,” prompts should provide detailed instructions to demonstrate accessibility best practices rather than pointing at often vague guidelines like WCAG.”</p><p><strong>Integrated Accessibility Validation</strong>: IDE integrations should include real-time accessibility linting of AI-generated code, providing immediate feedback and suggestions for improvement.</p><p><strong>Community-Contributed Training Data</strong>: We should coordinate our efforts to produce an open source, high-quality accessible code dataset so that this data can be integrated into future models.</p><hr><p>The data from this project provides a roadmap for where to focus these efforts. We’re not dealing with models that are fundamentally incapable of generating accessible code — we’re dealing with models that haven’t been properly trained to prioritize accessibility by default.</p><h2 id="want-to-get-involved%3F" tabindex="-1"><a class="header-anchor" href="#want-to-get-involved%3F" aria-hidden="true">#</a> Want to Get Involved?</h2><p>If you want to conduct similar evaluations with your preferred models or specific use cases, I’ve created a template repository with the testing framework: <a href="https://github.com/aarongustafson/CodeGen-Model-Eval-and-Refine-Tools">CodeGen Model Eval and Refine Tools</a>. It includes the Python testing harness, prompt templates, and analysis guidelines to get you started.</p><p>The complete findings, methodology details, and code samples for my research are available <a href="https://github.com/aarongustafson/testing-llm-code-a11y">on GitHub</a>. I encourage you to dig into the data — it’s eye-opening and frustrating, yes, but ultimately actionable.</p><p>There are other projects and research exploring this space as well. A few worth checking out:</p><ul><li><a href="https://aimac.ai/">AIMAC</a> - The AI Model Accessibility Checker (AIMAC) Leaderboard measures how well LLMs generate accessible HTML pages using neutral prompts without specific accessibility guidance. Checks are performed with axe-core.</li><li><a href="https://github.com/microsoft/a11y-llm-eval">A11y LLM Evaluation Harness and Dataset</a> - A more recent research project to evaluate how well various LLM models generate accessible HTML content.</li></ul><hr><p>We’re at a critical moment where the patterns established in AI-assisted development will shape the accessibility of the web for years to come. We can either let this technology amplify existing accessibility problems, or we can tackle the problems head-on and be part of the solution.</p>]]></content><amg:twitter><![CDATA[I probed an LLM’s responses to HTML code generation prompts to assess its adherence to accessibility best practices. The results showed key areas where better training data is needed.]]></amg:twitter><amg:summary><![CDATA[<p>Late last year, I probed an LLM’s responses to HTML code generation prompts to assess its adherence to accessibility best practices. The results were unsurprisingly disappointing — roughly what I’d expect from a developer aware of accessibility but unsure how to implement it. The study highlighted key areas where training data needs improvement.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Late last year, I probed an LLM’s responses to HTML code generation prompts to assess its adherence to accessibility best practices. The results were unsurprisingly disappointing — roughly what I’d expect from a developer aware of accessibility but unsure how to implement it. The study highlighted key areas where training data needs improvement.</p>]]></summary><category term="accessibility" /><category term="AI/ML" /><category term="HTML" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/posts/2025-10-15/hero.png" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/learning-web-design-6th-edition-is-out/</id><title type="html"><![CDATA[Learning Web Design, 6th Edition is out!]]></title><link href="https://www.aaron-gustafson.com/notebook/learning-web-design-6th-edition-is-out/" rel="alternate" type="text/html" /><published>2025-07-11T00:25:40Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>It’s here! Nearly 900 pages of guidance on how to build web pages, to which I contributed a little over 100 pages: <a href="https://www.oreilly.com/library/view/learning-web-design/9781098137670/"><cite>Learning Web Design, 6th Edition</cite></a>.</p><p>Congrats to <a href="https://www.linkedin.com/in/jennifer-niederst-robbins">Jennifer</a> on the book and many thanks for bringing me along for the ride!</p>]]></content><amg:twitter><![CDATA[It’s here! Nearly 900 pages of guidance on how to build web pages, to which I contributed a little over 100 pages: Learning Web Design, 6th Edition.]]></amg:twitter><amg:summary><![CDATA[<p>It’s here! Nearly 900 pages of guidance on how to build web pages, to which I contributed a little over 100 pages: <a href="https://www.oreilly.com/library/view/learning-web-design/9781098137670/"><cite>Learning Web Design, 6th Edition</cite></a>.</p>]]></amg:summary><summary type="html"><![CDATA[<p>It’s here! Nearly 900 pages of guidance on how to build web pages, to which I contributed a little over 100 pages: <a href="https://www.oreilly.com/library/view/learning-web-design/9781098137670/"><cite>Learning Web Design, 6th Edition</cite></a>.</p>]]></summary><category term="writing" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/posts/2025-07-10/lwd-6e.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/passing-your-css-theme-to-canvas/</id><title type="html"><![CDATA[Passing Your CSS Theme to `canvas`]]></title><link href="https://www.aaron-gustafson.com/notebook/passing-your-css-theme-to-canvas/" rel="alternate" type="text/html" /><published>2025-05-01T21:49:27Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>While working on a recent project I noticed an issue with a <code>canvas</code>-based audio visualization when I toggled between light and dark modes. When I’d originally set it up I was browsing in dark mode and the light visualization stroke showed up perfectly on the dark background, but it was invisible when viewed using the light theme (which I’d neglected to test). I searched around, but didn’t find any articles on easy ways to make <code>canvas</code> respond nicely to user preferences, so I thought I’d share (in brief) how I solved it.</p><h2 id="the-css-setup" tabindex="-1"><a class="header-anchor" href="#the-css-setup" aria-hidden="true">#</a> The CSS Setup</h2><p>The theming of this particular project uses <a href="https://developer.mozilla.org/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties">CSS custom properties</a>. For simplicty I’m going to set up two named colors and then use two theme-specific custom properties to apply them in the default light theme and the dark theme:</p><pre class="language-css" tabindex="0"><code class="language-css"><span class="token selector">:root</span><span class="token punctuation">{</span><span class="token property">–color-dark</span><span class="token punctuation">:</span> #222<span class="token punctuation">;</span><span class="token property">–color-light</span><span class="token punctuation">:</span><span class="token function">rgba</span><span class="token punctuation">(</span>255<span class="token punctuation">,</span> 255<span class="token punctuation">,</span> 255<span class="token punctuation">,</span> 0.5<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">–color-background</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-light<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">–color-foreground</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-dark<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token atrule"><span class="token rule">@media</span><span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> dark<span class="token punctuation">)</span></span><span class="token punctuation">{</span><span class="token selector">:root</span><span class="token punctuation">{</span><span class="token property">–color-background</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-dark<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token property">–color-foreground</span><span class="token punctuation">:</span><span class="token function">var</span><span class="token punctuation">(</span>–color-light<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span></code></pre><h2 id="applying-the-theme-to-canvas" tabindex="-1"><a class="header-anchor" href="#applying-the-theme-to-canvas" aria-hidden="true">#</a> Applying the Theme to Canvas</h2><p>To get the theme into my <code>canvas</code>-related code, I set up a <code>theme</code> object to hold the values:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">const</span> theme <span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre><p>Next, I wrote a function to pull in the theme colors using <a href="https://developer.mozilla.org/docs/Web/API/Window/getComputedStyle"><code>window.getComputedStyle()</code></a>. After defining the function, I call it immediately to populate the <code>theme</code> object:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">function</span><span class="token function">importTheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span>theme<span class="token punctuation">.</span>foreground <span class="token operator">=</span>window<span class="token punctuation">.</span><span class="token function">getComputedStyle</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>documentElement<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getPropertyValue</span><span class="token punctuation">(</span><span class="token string">“–color-foreground”</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">||</span><span class="token string">“black”</span><span class="token punctuation">;</span>theme<span class="token punctuation">.</span>background <span class="token operator">=</span>window<span class="token punctuation">.</span><span class="token function">getComputedStyle</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>documentElement<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getPropertyValue</span><span class="token punctuation">(</span><span class="token string">“–color-background”</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">||</span><span class="token string">“white”</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token function">importTheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>I set this up with just two theme colors, but you can import as many (or few) as you like. Be sure to set a sensible default or fallback for each color though, just in case your theme’s custom property names change.</p><p>With this in place, I can set my <code>canvas</code> animation’s colors by referencing them from the <code>theme</code> object. For example:</p><pre class="language-js" tabindex="0"><code class="language-js">context<span class="token punctuation">.</span>fillStyle <span class="token operator">=</span> theme<span class="token punctuation">.</span>foreground<span class="token punctuation">;</span></code></pre><h2 id="keeping-things-in-sync" tabindex="-1"><a class="header-anchor" href="#keeping-things-in-sync" aria-hidden="true">#</a> Keeping Things in Sync</h2><p>The final bit of magic comes when you add an event listener to a <code>MediaQueryList</code>:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">const</span> mediaQuery <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">“(prefers-color-scheme: dark)”</span><span class="token punctuation">)</span><span class="token punctuation">;</span>mediaQuery<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“change”</span><span class="token punctuation">,</span> importTheme<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Here I’ve used <code>matchMedia()</code> to get a <code>MediaQueryList</code> object. Typically we use the <code>matches</code> property of this object to establish whether the media query currently matches or not. A lesser-known option, however, is that you can attach an event listener to it that will be triggered whenever the query’s status changes. So cool! With this in place, the <code>canvas</code> contents will update whenever the user’s theme changes. <a href="#fig-2025-05-01-01">Here’s an example of that</a>:</p><figure id="fig-2025-05-01-01" class="media-container"><p><a href="https://www.youtube.com/watch?v=pALIuO5uHUA">https://www.youtube.com/watch?v=pALIuO5uHUA</a></p><figcaption><p>This video demonstrates how a canvas element rendering a dark sine wave against a light background can miraculously transform into a light sine wave against a dark background using CSS custom properties and a bit of JavaScript.</p></figcaption></figure><h1 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h1><p>I put together <a href="https://codepen.io/aarongustafson/pen/LEEQyqg">a quick demo of this</a> in a fork of <a href="https://codepen.io/alvinshaw/pen/mdEKggg">Alvin Shaw’s Canvas Sine Wave Experiment</a>:</p><figure id="fig-2025-05-01-02" class="media-container"><iframe class="codepen" height="331" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/LEEQyqg?height=331&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/aarongustafson/pen/LEEQyqg" target="_blank" rel="noopener">See the Pen</a></p></iframe></figure><hr><p>Hopefully this is helpful to someone out there. Happy theming!</p>]]></content><amg:twitter><![CDATA[Need to pipe your CSS theme into a `canvas` element? Here’s how I did it.]]></amg:twitter><amg:summary><![CDATA[<p>While working on a recent project I noticed an issue with a <code>canvas</code>-based audio visualization when I toggled between light and dark modes. When I’d originally set it up I was browsing in dark mode and the light visualization stroke showed up perfectly on the dark background, but it was invisible when viewed using the light theme (which I’d neglected to test). I searched around, but didn’t find any articles on easy ways to make <code>canvas</code> respond nicely to user preferences, so I thought I’d share (in brief) how I solved it.</p>]]></amg:summary><summary type="html"><![CDATA[<p>While working on a recent project I noticed an issue with a <code>canvas</code>-based audio visualization when I toggled between light and dark modes. When I’d originally set it up I was browsing in dark mode and the light visualization stroke showed up perfectly on the dark background, but it was invisible when viewed using the light theme (which I’d neglected to test). I searched around, but didn’t find any articles on easy ways to make <code>canvas</code> respond nicely to user preferences, so I thought I’d share (in brief) how I solved it.</p>]]></summary><category term="accessibility" /><category term="animation" /><category term="CSS" /><category term="design" /><category term="JavaScript" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/exploring-ais-role-in-accessibility-with-gymnasium/</id><title type="html"><![CDATA[Exploring AI’s Role in Accessibility]]></title><link href="https://www.aaron-gustafson.com/notebook/exploring-ais-role-in-accessibility-with-gymnasium/" rel="alternate" type="text/html" /><published>2025-04-14T16:11:54Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Earlier this month I joined Jeremy Osborne and Andrew Miller to talk about the benefits and harms of AI as it relates to accessibility. It was livestreamed on a few platforms, but I wanted to drop links to the transcript and archival video in case you’re interested.</p><figure><p><a href="https://www.youtube.com/watch?v=7ObB4jM-QXU">https://www.youtube.com/watch?v=7ObB4jM-QXU</a></p></figure><p><a href="https://github.com/gymnasium/exploring-ais-role-in-accessibility">View the transcript</a></p>]]></content><amg:twitter><![CDATA[I met up with Jeremy Osborne and Andrew Miller to talk about the benefits and harms of AI as it relates to accessibility.]]></amg:twitter><amg:summary><![CDATA[<p>Earlier this month I joined Jeremy Osborne and Andrew Miller to talk about the benefits and harms of AI as it relates to accessibility. It was livestreamed on a few platforms, but I wanted to drop links to the transcript and archival video in case you’re interested.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Earlier this month I joined Jeremy Osborne and Andrew Miller to talk about the benefits and harms of AI as it relates to accessibility. It was livestreamed on a few platforms, but I wanted to drop links to the transcript and archival video in case you’re interested.</p>]]></summary><category term="accessibility" /><category term="AI/ML" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/complaining-about-designers-fiddling-with-figma-solves-nothing/</id><title type="html"><![CDATA[Complaining About Designers Fiddling with Figma Solves Nothing]]></title><link href="https://www.aaron-gustafson.com/notebook/complaining-about-designers-fiddling-with-figma-solves-nothing/" rel="alternate" type="text/html" /><published>2025-03-24T16:11:54Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Michael F. Buckley posted <a href="https://uxdesign.cc/figmas-not-a-design-tool-it-s-a-rube-goldberg-machine-for-avoiding-code-2a24f11add5d">a somewhat imflamatory piece to the UX Collective blog over on Medium</a> and I had some strong reactions to it I wanted to share. You should read it first before continuing.</p><p>First off, I agree with his core message: designers should understand the medium they are working in, be it web or an OS-specific UI. That’s where my agreements end, however.</p><blockquote><p>They pat themselves on the back, believing they’ve mastered digital design. Meanwhile, a developer glances at the file, sighs, and codes the button in five minutes.</p></blockquote><p>Condescention toward designers aside, this also assumes a high degree of competency and understanding of nuance on the engineering side which — in my experience — is not guaranteed. I cannot tell you how often I’ve designed an interface that calls for a button to submit a form and I get back a <code>div</code> with a click handler that can’t accept focus and has no design considerations for hover, focus, etc. In other words, if I provided a design with a single button on it, I get that button exactly. Sometimes exactly to those explicit dimensions, text overflow be damned.</p><p>There needs to be an acknowledgement and appreciation that designers know their craft and (generally) have good reasons for being overly prescriptive. Do some designers stray into realms of self-indulgence… absolutely! But so do engineers.</p><p>I think where we need to get to is a place where handoff provides enough information to inform the engineer of the totality of what’s needed. Sure, to a seasoned engineer that may seem overly detailed, but to a junior engineer it can be incredibly helpful in learning all of the considerations of something as seemingly simple as a button. And even a seasoned engineer might learn a thing (or three) from annotations regarding accessibility and such. Communication is the key to success here.</p><p>Also, some prototyping in a tool like Figma can help with catching issues — like keyboard traps, for instance — before they are codified into code. Making changes in Figma or other design tools is generally a lot cheaper that doing it in code. Back of the napkin math — which is generally agreed upon across the industry — is that catching &amp; fixing an accessibility issue in design costs about $100. In development, it’s like $1,000. If it sneaks into production, it’s gonna be more like $10,000.</p><p>To be clear, I’m not disagreeing with the piece in its entirety — designers should learn their medium (which is coding to a degree but also UX expectations) and Figma’s <strong>not</strong> the medium — but I think there’s more nuance to it. I also dislike the us vs. them framing; we’re all in this together.</p>]]></content><amg:twitter><![CDATA[Michael F. Buckley posted a somewhat imflamatory piece to the UX Collective blog over on Medium and I had some strong reactions to it I wanted to share.]]></amg:twitter><amg:summary><![CDATA[<p>Michael F. Buckley posted <a href="https://uxdesign.cc/figmas-not-a-design-tool-it-s-a-rube-goldberg-machine-for-avoiding-code-2a24f11add5d">a somewhat imflamatory piece to the UX Collective blog over on Medium</a> and I had some strong reactions to it I wanted to share. You should read it first before continuing.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Michael F. Buckley posted <a href="https://uxdesign.cc/figmas-not-a-design-tool-it-s-a-rube-goldberg-machine-for-avoiding-code-2a24f11add5d">a somewhat imflamatory piece to the UX Collective blog over on Medium</a> and I had some strong reactions to it I wanted to share. You should read it first before continuing.</p>]]></summary><category term="design" /><category term="web development" /><category term="industry" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/on-diversity/</id><title type="html"><![CDATA[On Diversity]]></title><link href="https://www.aaron-gustafson.com/notebook/on-diversity/" rel="alternate" type="text/html" /><published>2025-01-30T23:11:54Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I’ve been broadly working in the DEI (or DEIA if you like) sphere for decades now. Most of my work has been coming at it from the accessibility side of things, but I got really involved in allyship and more traditional DEI work starting in 2019. Seeing <a href="https://www.npr.org/2025/01/23/nx-s1-5271588/trump-dei-diversity-equity-inclusion-federal-workers-government">the current U.S. administration taking an axe to DEI programs in the government</a> and <a href="https://www.forbes.com/sites/saradorn/2025/01/23/trumps-diversity-orders-rattle-ceos-what-companies-should-know-about-new-dei-rules/">bully private businesses to do the same</a> has me incredibly frustrated, confused, and (yes) angry. I want more equality and more opportunity in the world, not less.</p><p>And so, when I was listening to <a href="https://www.youtube.com/watch?v=mQwJuayXJ18">the latest episode of <cite>The Weekly Show with Jon Stewart</cite></a>, I was struck by how the left and right may actually be more aligned on DEI than the headlines lead us to believe.</p><p>In the episode, Stewart was interviewing former New Jersey Governor Chris Christie, a Republican. When the topic of DEI came up, they got into a discussion of merit vs. diversity in the context of the Secretary of Defense role. Both agreed that, in terms of merit, <a href="https://wikipedia.org/wiki/Lloyd_Austin">General Lloyd Austin</a> was a much better hire than <a href="https://en.wikipedia.org/wiki/Pete_Hegseth">Fox’s former weekend host Pete Hegseth</a>. The fact that Austin is also Black has no more impact on his being a better candidate than the fact that Pete Hegseth being White makes him a worse candidate. What Austin does bring to the table, however, is first-hand knowledge of what it’s like to rise up the ranks as a Black soldier. That’s a significant knowledge gap when it comes to the U.S. military, whose top brass isn’t representative of the diversity of its personnel.</p><p>This is something that Christie actually points out when discussing becoming the U.S. Attorney in New Jersey back in 2002:</p><blockquote><p>When I got there, I just did a lot of walking around the office to see, <em>okay, who’s here?</em> Jon, it was the whitest, malest office I had ever been in in my life. And I was coming from private law practice.</p></blockquote><p>He, rightly, saw this as a problem and wanted to address it. He told his staff</p><blockquote><p>[We need] to go out and recruit candidates who are African-American, Latino, Asian, women. Bring them to me. If they’re not good, I’m not going to hire them. But I’m convinced we’re not seeing them.</p></blockquote><p>His approach to address this was perfectly rational and aligned with the approach Jon had discussed mere moments before:</p><blockquote><p>What I found was hiring has a certain inertia to it, right? Generally, the people that started whatever industry or whatever office did, generally hire close to people that resemble them. So I’m not even talking about White/Black. I’m talking about like… I’ll just go with late-night comedy, right?</p><p>David Letterman revolutionized late-night comedy. He did it with a lot of Harvard, Lampoon, SNL, same way, writers. The comedy writing industry was for a long time — not necessarily out of malevolence or prejudice — the inertia of it, the status quo of it, was nerdy white dudes from Harvard and the other Ivy Leagues.</p><p>But even when we went to like, “Oh, we’re going to do blind submissions,” what we didn’t realize is all the agents are also steeped in that same status quo. So all the resumes — even when we would get them — still predominantly [trails off]. When we went specifically to say — now, this is what you would consider DEI — “Give us not that. Open it up to make sure you give us women, people of color, other writers, so that we can at least see what that is.” And all of a sudden, we found these incredible writers. Now, you could say, “Oh, you put diversity over competence,” but that’s the red herring. We didn’t. We opened up what were stagnant pools. Pools that were incestuous. And we opened up those tributaries. Isn’t that what increases competition, not decreases it?</p></blockquote><p>What’s fascinating here is that they are both making the same point. As Christie says later</p><blockquote><p>We then went about this process of hiring a large number of African-American, Latino, and Asian prosecutors, but I would tell you that every one of them checked both boxes. They checked the box of, “they now look more like the community we represent than we did before.” And these are really good lawyers.</p></blockquote><p>So these two men from very different political viewpoints totally agree on the importance of <em>representation</em>. So where’s the issue?</p><h2 id="the-issue-is-tokenism" tabindex="-1"><a class="header-anchor" href="#the-issue-is-tokenism" aria-hidden="true">#</a> The Issue is Tokenism</h2><p>When Stewart highlighted how aligned their two perspectives were and Christie pushed back, stating that DEI policies were problematic:</p><blockquote><p>I think there have been a number of areas where there are people who hire certain folks just for their diversity. I’ve seen it happen here in New Jersey, in the government since I left. Where people say, “I am going to make sure that I have one of every…” It’s almost like a half a Noah’s Ark. “I’m going to have one of these and one of these and one one of these and one of these.”</p></blockquote><p>Stewart questioned that:</p><blockquote><p>But you just told me that’s what you did in the prosecutor’s office.</p></blockquote><p>But Christie didn’t see it that way:</p><blockquote><p>No, what I did was get them in to interview them. If it turned out, Jon, that they were also really good lawyers, they got hired. I’m talking about something different. I’m talking about predetermining the outcome in the way that you just talked about — and I believe that legacy admissions predetermined the outcome — that there have been some in charge of government across this country who have predetermined determined outcomes and said, “I am going to have this many African-Americans, this many Latinos, this many Asians, this many lesbians, this many gay men…” I think that when people see that, they say to themselves, “That’s not right either.”</p></blockquote><p>What he’s talking about is what I’d call <em>performative DEI</em>. It’s not substantive, but attempts to give off the appearance of being so. It’s the DEI equivalent of <a href="https://wikipedia.org/wiki/Greenwashing">greenwashing</a>.</p><h2 id="dei-cannot-be-performative" tabindex="-1"><a class="header-anchor" href="#dei-cannot-be-performative" aria-hidden="true">#</a> DEI Cannot be Performative</h2><p>When people hire folks or celebrate folks for their diversity rather than their diversity plus their competence or talents, it undermines the legitimacy of DEI programs that are attempting to do what they both discussed being important: <em>representation</em>.</p><p>As they both said, we need to <a href="https://blog.skill.jobs/screening-in-vs-screening-out-shifting-recruitment-strategies-for-better-hiring-outcomes/">screen in</a> job applicants who wouldn’t otherwise consider applying for roles in our organizations. Christie talked about this too:</p><blockquote><p>The aha moment for me on that concept and why it was the right way to go was there was a young guy that I hired very early on: African-American, University of Michigan, University of Penn Law School, clerk for Alan Page — the former Minnesota Viking, defensive tackle in the Supreme Court of Minnesota— He’s from New Jersey, grew up in Maplewood. I said to him, “Why didn’t you ever apply here before?” He said, “Because I knew people like me wouldn’t get hired.”</p></blockquote><p>Hiring is just part of the process though. You can widen the applicant funnel and bring in a more representative — which is to say <em>diverse</em> — applicant pool with relatively little effort. Where things often fall short is retention.</p><h2 id="is-your-organization-even-ready%3F" tabindex="-1"><a class="header-anchor" href="#is-your-organization-even-ready%3F" aria-hidden="true">#</a> Is Your Organization Even Ready?</h2><p>If your organization isn’t excited at the prospect of a more diverse workforce and prepared to support them when they are onboarded, you need to press pause and get prepared. Similarly, if your company is eager, but very homogenous, you’ve also got work to do. No one wants to come into a job and feel like “the only” or “the token” anything. And even if they were the most qualified applicant for the position, some jackass will say something that implies they are. It’s a tale as old as time and you need to be prepared for that reality.</p><p>The first thing you need to do is <em>educate</em>. You need to help folks on your team understand the gaps in your collective knowledge &amp; experience. They need to see that a more diverse team can help fill those gaps. The data that shows that <a href="https://www.forbes.com/councils/forbestechcouncil/2022/05/10/diverse-teams-achieve-greater-success-how-business-can-champion-diversity-as-good-sense/">more diverse organizations are more successful</a>. Share that! I’m guessing most of your team is there because they want your organization to be as successful as possible.</p><p>And make sure they understand the historical barriers folks from different communities have faced in getting access to jobs at organizations like yours… even when they were equally or more accomplished than folks from the dominant group. As Stewart said on the show:</p><blockquote><p>It’s not rigging [the system] in a different direction, it’s unrigging it.</p></blockquote><p>It’s also important to note that the process here needs to be inclusive as well… call people <em>in</em>, don’t call them <em>out</em>. Everyone is on their own journey and deserves the space to fail and learn from their mistakes. If someone says something offensive, let them know that it’s offensive and why. Tell them what they should say — if anything — instead.</p><p>If you approach people with empathy, you’re much more likely to get a positive response. And, Twitter aside, most folks aren’t out in these streets trying to be trolls. People are a product of their own experiences and those experiences can be quite different from yours. Help your colleagues broaden their perspectives with positive reinforcement, not chastising.</p><p>That said, you also need the proper mechanisms in place to address non-inclusive behaviors when they become a pattern or reach a certain threshold of severity. Those mechanisms need to outline the consequences for such behavior. The severity of the consequence needs to align with the severity of the harm, but it may need to escalate in severity for repeat offenses. Depending on the size of your organization, coming up with these policies and consequences could be a group activity to ensure both awareness and buy-in.</p><h2 id="embrace-dei-and-pave-the-way-for-mediocrity" tabindex="-1"><a class="header-anchor" href="#embrace-dei-and-pave-the-way-for-mediocrity" aria-hidden="true">#</a> Embrace DEI and Pave the Way for Mediocrity</h2><p>To be clear, neither Jon Stewart nor the progressive left are pushing for diversity quotas like Christie seems to think they are. But there are folks out there who are. These performative DEI programs have got to go. As my colleague and friend Ebele Okoli says “bake it in, don’t cake it on.”</p><p>Don’t hire or promote someone just because you think their headshot would help to <em><a href="https://www.merriam-webster.com/dictionary/melanated">melanate</a></em> your About page. That’s not what DEI is about and it doesn’t help us to reverse the dire course the U.S. government and cowardly companies are taking currently. DEI needs to be a part of every process in your organization on order to give everyone — white men like me included — an equal chance to succeed.</p><p>Cast a wide net. Hire and promote for competence <em>and</em> to address the knowledge gaps your team absolutely has. Foster an inclusive workplace that <em>values</em> the different lived experience and perspectives brought to the table by each and every employee. That is how you succeed with DEI. It’s also how DEI will help your organization succeed in its mission and grow to hire more folks.</p><p>And as more of the incredibly talented people out there get hired on at organizations like yours, all boats will rise, creating more jobs and space for mediocre people of all stripes to get hired and rise up the ranks too. But they won’t get there just because or who they know, what they look like, or because they tick a particular box on your diversity bingo card.</p>]]></content><amg:twitter><![CDATA[Seeing the current U.S. administration taking an axe to DEI programs in the government and bully private businesses to do the same has me incredibly frustrated, confused, and (yes) angry. I want more equality and more opportunity in the world, not less.]]></amg:twitter><amg:summary><![CDATA[<p>I’ve been broadly working in the DEI (or DEIA if you like) sphere for decades now. Most of my work has been coming at it from the accessibility side of things, but I got really involved in allyship and more traditional DEI work starting in 2019. Seeing <a href="https://www.npr.org/2025/01/23/nx-s1-5271588/trump-dei-diversity-equity-inclusion-federal-workers-government">the current U.S. administration taking an axe to DEI programs in the government</a> and <a href="https://www.forbes.com/sites/saradorn/2025/01/23/trumps-diversity-orders-rattle-ceos-what-companies-should-know-about-new-dei-rules/">bully private businesses to do the same</a> has me incredibly frustrated, confused, and (yes) angry. I want more equality and more opportunity in the world, not less.</p><p>And so, when I was listening to <a href="https://www.youtube.com/watch?v=mQwJuayXJ18">the latest episode of <cite>The Weekly Show with Jon Stewart</cite></a>, I was struck by how the left and right may actually be more aligned on DEI than the headlines lead us to believe.</p>]]></amg:summary><summary type="html"><![CDATA[<p>I’ve been broadly working in the DEI (or DEIA if you like) sphere for decades now. Most of my work has been coming at it from the accessibility side of things, but I got really involved in allyship and more traditional DEI work starting in 2019. Seeing <a href="https://www.npr.org/2025/01/23/nx-s1-5271588/trump-dei-diversity-equity-inclusion-federal-workers-government">the current U.S. administration taking an axe to DEI programs in the government</a> and <a href="https://www.forbes.com/sites/saradorn/2025/01/23/trumps-diversity-orders-rattle-ceos-what-companies-should-know-about-new-dei-rules/">bully private businesses to do the same</a> has me incredibly frustrated, confused, and (yes) angry. I want more equality and more opportunity in the world, not less.</p><p>And so, when I was listening to <a href="https://www.youtube.com/watch?v=mQwJuayXJ18">the latest episode of <cite>The Weekly Show with Jon Stewart</cite></a>, I was struck by how the left and right may actually be more aligned on DEI than the headlines lead us to believe.</p>]]></summary><category term="equality" /><category term="inclusion" /><category term="society" /><category term="industry" /><category term="accessibility" /><category term="empathy" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/a-web-component-for-conditional-dependent-fields/</id><title type="html"><![CDATA[A Web Component for Conditional Dependent Fields]]></title><link href="https://www.aaron-gustafson.com/notebook/a-web-component-for-conditional-dependent-fields/" rel="alternate" type="text/html" /><published>2024-08-14T03:26:03Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>A few weeks back I released <a href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/">a web component to enable you to add requirement rules to checkbox groups</a>. Continuing in the form utility space, I’ve created a new web component that allows you to make fields required based on the values of other fields: <code>form-required-if</code>.</p><p>The <code>form-required-if</code> web component, which is based on <a href="https://github.com/easy-designs/jquery.easy-dependent-required-fields.js">a jQuery plugin I’d written in 2012</a>, looks like this:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>email=<em><span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Required if there’s an email value<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>depends-on-email<span class="token punctuation">&quot;</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>You wrap any field (and its <code>label</code>) in the component and then declare the conditions under which it should be required in the <code>conditions</code> attribute.</p><h2 id="defining-the-requirement-conditions" tabindex="-1"><a class="header-anchor" href="#defining-the-requirement-conditions" aria-hidden="true">#</a> Defining the requirement conditions</h2><p>Each condition is a key/value pair where the key aligns to the <code>name</code> of the field you need to observe and the value is the value that could trigger the dependency. If any value should trigger the dependency, you use an asterisk (</em>) as the value. In the example above, the field will become required when any value is assigned to the field matching <code>[name=&quot;email&quot;]</code>.</p><p>This <code>conditions</code> attribute can be populated with as many or as few dependencies as make sense for your use case. Multiple conditions are separated by double vertical pipes (<code>||</code> a.k.a. <em>or</em>) as in this example:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>email=<em>||test=3<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Depends on email or test field<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>depends-on-email-or-test<span class="token punctuation">“</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>Here the field depends on one of the following conditions being true:</p><ol><li>the field matching <code>[name=&quot;email&quot;]</code> has a value, <em>or</em></li><li>the field matching <code>[name=&quot;test&quot;]</code> has a value of “3”</li></ol><p>If the field you reference doesn’t exist, no errors will be thrown, it will just quietly exit.</p><h2 id="visually-indicating-a-field-is-required" tabindex="-1"><a class="header-anchor" href="#visually-indicating-a-field-is-required" aria-hidden="true">#</a> Visually indicating a field is required</h2><p>If you typically use an asterisk or similar to indicate a field is required, this web component can support that through one or both of the following attributes:</p><ul><li><code>indicator</code> - This attribute is where you define the indicator itself. It could be something as simple as a string (e.g., <em>), or even full-blown HTML.</li><li><code>indicator-placement</code> - As you can probably guess, this attribute is used to set the position of the indicator. If you want it at the start of the label text, you give it the value “before.” If you want it after the text, you use “after” or don’t use the attribute at all. Indicators will be placed after the label text by default.</li></ul><p>Here’s an example with a custom indicator that is HTML:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form-required-if</span><span class="token attr-name">conditions</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>email=</em><span class="token punctuation">”</span></span><span class="token attr-name">indicator</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>&lt;b&gt;</em>&lt;/b&gt;<span class="token punctuation">“</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">&gt;</span></span>Depends on email<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">”</span>dep2<span class="token punctuation">&quot;</span></span><span class="token punctuation">/&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form-required-if</span><span class="token punctuation">&gt;</span></span></code></pre><p>If you don’t include markup in your indicator, it will be automatically wrapped in <code>span</code> when injected into the DOM. The <code>hidden</code> and <code>aria-hidden</code> attributes are used to toggle its visibility, relative to the requirement state of the field.</p><h2 id="demo" tabindex="-1"><a class="header-anchor" href="#demo" aria-hidden="true">#</a> Demo</h2><p>I put together <a href="https://aarongustafson.github.io/form-required-if/demo/">a comprehensive demo of the web component</a> over on GitHub:</p><figure class="video-embed video-embed--4x3"><fullscreen-control class="talk__slides__embed video-embed__video"><iframe src="https://aarongustafson.github.io/form-required-if/demo/" class="talk__slides__embed video-embed__video" frameborder="0"></iframe></fullscreen-control></figure><h2 id="grab-it" tabindex="-1"><a class="header-anchor" href="#grab-it" aria-hidden="true">#</a> Grab It</h2><p>You can view the entire project (and suggest enhancements) over on <a href="https://github.com/aarongustafson/form-required-if">the <code>form-required-if</code> component’s Github repo</a>.</p>]]></content><amg:twitter><![CDATA[Sometimes you need to require a form field only if another field is filled in or has a specific value. Here’s a web component to enable that.]]></amg:twitter><amg:summary><![CDATA[<p>A few weeks back I released <a href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/">a web component to enable you to add requirement rules to checkbox groups</a>. Continuing in the form utility space, I’ve created a new web component that allows you to make fields required based on the values of other fields: <code>form-required-if</code>.</p>]]></amg:summary><summary type="html"><![CDATA[<p>A few weeks back I released <a href="https://www.aaron-gustafson.com/notebook/requirement-rules-for-checkboxes/">a web component to enable you to add requirement rules to checkbox groups</a>. Continuing in the form utility space, I’ve created a new web component that allows you to make fields required based on the values of other fields: <code>form-required-if</code>.</p>]]></summary><category term="forms" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="web components" /><category term="web forms" /></entry></feed>