<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mica</title>
    <description>The latest articles on DEV Community by Mica (@micaavigliano).</description>
    <link>https://dev.to/micaavigliano</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F578862%2Ff9c579bb-1a47-4cbc-a285-d5f8fc71f8ff.jpg</url>
      <title>DEV Community: Mica</title>
      <link>https://dev.to/micaavigliano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/micaavigliano"/>
    <language>en</language>
    <item>
      <title>Accessible and Functional Quantity Spinbutton Pattern</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Wed, 02 Sep 2026 20:42:40 +0000</pubDate>
      <link>https://dev.to/micaavigliano/accessible-and-functional-quantity-spinbutton-pattern-1945</link>
      <guid>https://dev.to/micaavigliano/accessible-and-functional-quantity-spinbutton-pattern-1945</guid>
      <description>&lt;p&gt;Hey! This is Mica writing, a human not an AI agent (nothing against them, not discrimination of any kind is allowed here). Just a brief disclaimer before starting: this post was non AI-generated (I am not going to lie, I tried to use it for some research but since the errors I found were not properly documented the AI invented answers, lol) because I felt the urgent need to start using my brain and hands to craft something from scratch. If you are a human, let's connect, at the end you will find the ways to reach me. I am on a doom scrolling detox so you won't find me on instagram or twitter now. So, I have a question: &lt;a href="https://www.youtube.com/watch?v=Aiu5Fr7-hFQ&amp;amp;list=RDAiu5Fr7-hFQ&amp;amp;start_radio=1" rel="noopener noreferrer"&gt;Is there anybody out there?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you work on an e-commerce, a booking (hotels, flights, trains, etc) platform, a restaurant reservation system maybe you are familiar with the quantity spinbutton component. It is a common UI pattern that allows users to select a quantity of items they want to purchase or book. It is configured by two buttons and one input; one button for increasing the quantity, another button to decrease the quantity and an input where the user is able to type the desired amount or increase/decrease the quantity using the Up Arrow or Down Arrow accordingly. Sounds simple, and yes it is, but it is also a critical flow in the user journey. If it's badly implemented, you can not only be potentially losing sales (translation: losing money, which is important) but also creating a frustrating experience for users with disabilities (which should be more important than money, but those are my morals and ethics, lol). My goal in this article is setting a clear basis of a non-negotiable structure and behavior of the &lt;strong&gt;&lt;u&gt;quantity spinbutton pattern&lt;/u&gt;&lt;/strong&gt; and from there, explore different variations depending on the use case.&lt;/p&gt;

&lt;p&gt;Disclaimer: This is not a written-in-stone guide, I can make mistakes, so please let me know if you disagree with something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expected behaviors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Keyboard
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Up Arrow&lt;/td&gt;
&lt;td&gt;Increases the value by its step value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Down Arrow&lt;/td&gt;
&lt;td&gt;Decreases the value by its step value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;If the spinbutton has a minimum value, sets the value to its minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End&lt;/td&gt;
&lt;td&gt;If the spinbutton has a maximum value, sets the value to its maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Command or Fn + Right Arrow
&lt;/td&gt;
&lt;td&gt;Substitute for Apple's keyboards that do not have a End key to set the value to its maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Command or Fn + Left Arrow
&lt;/td&gt;
&lt;td&gt;Substitute for Apple's keyboards that do not have a End key to set the value to its minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Screen Readers
&lt;/h3&gt;

&lt;h4&gt;
  
  
  NVDA Navigation
&lt;/h4&gt;

&lt;p&gt;You can download NVDA &lt;a href="https://www.nvaccess.org/download/" rel="noopener noreferrer"&gt;here&lt;/a&gt; and it's only available for the Windows operating system. It is compatible with Chrome, Firefox and Edge browsers.&lt;br&gt;
Long story short, an NVDA modifier key is needed to navigate using this screen reader. The modifier key can be Insert key (by default) or it can be remapped to the Caps Lock in the settings. Two modes for different purposes can be found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browse Mode&lt;/strong&gt;: Browse mode is used when reading documents or web pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus Mode&lt;/strong&gt;: Focus mode is used when the user enters a form or other fields that require user input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NVDA automatically switches between Browse and Focus modes, but the user can toggle them using Insert + Space Bar.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
Down Arrow (Browse Mode)&lt;/td&gt;
&lt;td&gt;Read next item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Enter or Space Bar
&lt;/td&gt;
&lt;td&gt;Activate button&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Down Arrow (Focus Mode)&lt;/td&gt;
&lt;td&gt;Decrement by step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Up Arrow (Focus Mode)&lt;/td&gt;
&lt;td&gt;Increment by step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;Jump to aria-valuemin (via your custom handler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End&lt;/td&gt;
&lt;td&gt;Jump to aria-valuemax (via your custom handler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab&lt;/td&gt;
&lt;td&gt;Commit value, leave the field, return to browse mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  VoiceOver Navigation
&lt;/h4&gt;

&lt;p&gt;If you are a macOS user, VoiceOver comes built into the operating system of your MacBook. It's compatible with the Safari browser.&lt;br&gt;
VoiceOver uses the Control and Option keys before each command. This combination is called &lt;strong&gt;VO&lt;/strong&gt; and these keys can be locked/unlocked by pressing Control + Option + ;(semicolon) all together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
Tab/Shift + Tab
&lt;/td&gt;
&lt;td&gt;Go to next/previous focusable item (link, button, input, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Space Bar
&lt;/td&gt;
&lt;td&gt;Activate a link or form control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Right Arrow
&lt;/td&gt;
&lt;td&gt;Move VO cursor to next element (not limited to focusable items)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Left Arrow
&lt;/td&gt;
&lt;td&gt;Move VO cursor to previous element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Shift + Down Arrow
&lt;/td&gt;
&lt;td&gt;Once the VO cursor is on the spinbutton, this combination enters the spinbutton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Up Arrow&lt;/td&gt;
&lt;td&gt;Once inside the spinbutton, increment by step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Down Arrow&lt;/td&gt;
&lt;td&gt;Once inside the spinbutton, decrement by step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;Jump to aria-valuemin (via your custom handler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End&lt;/td&gt;
&lt;td&gt;Jump to aria-valuemax (via your custom handler)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Gestures VoiceOver - iOS
&lt;/h4&gt;

&lt;p&gt;If you are an iOS user, VoiceOver is the screen reader by default on the operating system of your iPhone. It's compatible with the Safari browser. Unlike VoiceOver on macOS, VoiceOver on iOS, the navigation is controlled by finger gestures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gesture&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe next&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read next item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe left&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read previous item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Double-tap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Activate (link, button)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe up&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, increase the value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe down&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, decrease the value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Gestures Talkback - Android
&lt;/h4&gt;

&lt;p&gt;If you are an Android user, TalkBack is the default screen reader on the operating system of your mobile. It's compatible with Chrome, Firefox and Edge. Like VoiceOver for iOS, the navigation is controlled by gestures with the fingers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gesture&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe next&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read next item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe left&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read previous item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Double-tap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Activate (link, button)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe up&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, increase the value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe down&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, decrease the value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Pattern A: APG (ARIA Authoring Practices Guide) Spinbutton by W3C
&lt;/h2&gt;

&lt;p&gt;Currently everyone is following the &lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/" rel="noopener noreferrer"&gt;APG (ARIA Authoring Practices Guide)&lt;/a&gt; as if it were the bible and not as what it is: a &lt;strong&gt;&lt;u&gt;guide&lt;/u&gt;&lt;/strong&gt;, a very useful guide, btw, THE guide. Everything written there is considered a commandment written in stone by &lt;u&gt;non-accessibility experts&lt;/u&gt; (the people behind W3C are doing an amazing work and I only have gratitude and admiration towards them) and this might be a problem. The intention is good but the execution is not. When you first land on the &lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/examples/quantity-spinbutton/" rel="noopener noreferrer"&gt;APG Spin Button Pattern&lt;/a&gt;, the first thing you see is an alert with the following warning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code in this example is not intended for production environments. Before using it for any purpose, read this to understand why.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and then a list of reasons why you should be critical of the pattern provided. In this case, the most important one is:&lt;/p&gt;

&lt;blockquote&gt;Robust accessibility can be further optimized by choosing implementation patterns that maximize use of semantic HTML and heeding the warning that No ARIA is better than Bad ARIA.&lt;/blockquote&gt;

&lt;p&gt;So, what does this mean? Well, a couple of important things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Always be critical of everything (a rule for life, tho)&lt;/li&gt;
&lt;li&gt;Keep in mind the necessities and limitations of your product/business, and (more importantly) comply with the regulations and legislation in order to guarantee freedom of navigation through your product for users with disabilities.&lt;/li&gt;
&lt;li&gt;There is &lt;strong&gt;&lt;u&gt;always&lt;/u&gt;&lt;/strong&gt; (well, almost 80% always) an HTML equivalent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After this brief introduction to the APG, I would like to dive into its &lt;strong&gt;&lt;u&gt;Spinbutton pattern&lt;/u&gt;&lt;/strong&gt;. &lt;a href="https://www.w3.org/" rel="noopener noreferrer"&gt;W3C (The World Wide Web Consortium)&lt;/a&gt; presents us a solution using the WAI-ARIA &lt;code&gt;role="spinbutton"&lt;/code&gt; on the &lt;code&gt;input&lt;/code&gt; tag instead of using the native HTML solutions provided by the tag itself, the buttons are removed from the focus flow because they have a &lt;code&gt;tabindex="-1"&lt;/code&gt; but they are still operable via mouse and by using screen readers. Only the tag with the &lt;code&gt;role="spinbutton"&lt;/code&gt; is not fully semantic by itself and we will need to add the keyboard and screen reader functionalities programmatically and make it semantic by adding properties and states to the input element. &lt;/p&gt;

&lt;p&gt;In the accessibility field we have a mantra: &lt;strong&gt;No ARIA is better than Bad ARIA&lt;/strong&gt;. You should be extra careful when using ARIA because, if it is not used correctly, you will end up creating more issues than solving them. Again, the intention is good but the outcome is more barriers. Last month, &lt;a href="https://webaim.org/" rel="noopener noreferrer"&gt;WebAIM org&lt;/a&gt; launched its annual report for 2026 about the state of accessibility in over 1 million pages: &lt;a href="https://webaim.org/projects/million/" rel="noopener noreferrer"&gt;The WebAIM Million&lt;/a&gt;. The report shows that in 2026, accessibility errors increased by 10.1% according to the WCAG 2.2 Level A/AA conformance failures and concluded with the following reflection: &lt;/p&gt;

&lt;blockquote&gt;The 2026 WebAIM Million analysis found notable increases in both the number of detected accessibility errors and number of pages with WCAG conformance failures, reversing a trend of gradual accessibility improvements in recent years. A primary concern is the significant increase in home page complexity and ARIA code, both of which correlate to increased detectable errors.
These trends likely reflect broader shifts in web development including increased reliance on 3rd party frameworks and libraries and automated or AI-assisted coding practices (“vibe coding”). Home pages are getting larger and more technologically complex at an alarming rate, making accessibility more difficult to achieve and maintain. A key takeaway from this year's report is improving accessibility at scale will require both better practices and simpler systems. Alternatively, complex systems need to do a better job of focusing on accessibility fundamentals.&lt;/blockquote&gt;

&lt;p&gt;It's not a surprise that creating components using AI without questioning the output will end up creating more barriers for users with disabilities, and sloppier websites overall. Also, the lack of native and accessible examples on the web makes the AI coding agents return inaccessible components, but okay not everything is the fault of the AI, to be honest.&lt;/p&gt;
&lt;h3&gt;
  
  
  Functional Example – APG
&lt;/h3&gt;

&lt;p&gt;you can find the functional example &lt;a href="https://micaavigliano.com/en/blog/quantity-spinbutton-pattern#functional-example-apg" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  APG code
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"add-to-bag"&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"spinbutton"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
  &lt;span class="na"&gt;inputMode=&lt;/span&gt;&lt;span class="s"&gt;"numeric"&lt;/span&gt;
  &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"[0-9]*"&lt;/span&gt;
  &lt;span class="na"&gt;autoComplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;
  &lt;span class="na"&gt;spellCheck=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuemin=&lt;/span&gt;&lt;span class="s"&gt;{MIN}&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuemax=&lt;/span&gt;&lt;span class="s"&gt;{MAX}&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuenow=&lt;/span&gt;&lt;span class="s"&gt;{isValid&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="na"&gt;numeric&lt;/span&gt; &lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="na"&gt;undefined&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuetext=&lt;/span&gt;&lt;span class="s"&gt;{value}&lt;/span&gt;
  &lt;span class="na"&gt;aria-invalid=&lt;/span&gt;&lt;span class="s"&gt;{!isValid&lt;/span&gt; &lt;span class="err"&gt;||&lt;/span&gt; &lt;span class="na"&gt;undefined&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;aria-describedby=&lt;/span&gt;&lt;span class="s"&gt;{isValid&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;help-add-to-bag&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;help-add-to-bag&lt;/span&gt; &lt;span class="na"&gt;error-add-to-bag&lt;/span&gt;&lt;span class="err"&gt;'}&lt;/span&gt;
  &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;{value}&lt;/span&gt;
  &lt;span class="na"&gt;onChange=&lt;/span&gt;&lt;span class="s"&gt;{handleChange}&lt;/span&gt;
  &lt;span class="na"&gt;onKeyDown=&lt;/span&gt;&lt;span class="s"&gt;{handleKeyDown}&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt;: to relate the &lt;code&gt;&amp;lt;input /&amp;gt;&lt;/code&gt; with the &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; element&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role="spinbutton"&lt;/code&gt;: this is a &lt;a href="https://www.w3.org/TR/wai-aria/#spinbutton" rel="noopener noreferrer"&gt;WAI-ARIA role&lt;/a&gt; that allows the user to increase and decrease a value within a given range. This role can be used on &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; or on a non-semantic element such as &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. The APG pattern for the &lt;code&gt;spinbutton&lt;/code&gt; component proposes the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; with a &lt;code&gt;type="text"&lt;/code&gt; making the development more challenging.  The expected keyboard behavior for a &lt;code&gt;spinbutton&lt;/code&gt; is being able to increment or decrement the value by pressing the up/down arrow accordingly. Since this is not a native interaction of the &lt;code&gt;role="spinbutton"&lt;/code&gt; or of the &lt;code&gt;&amp;lt;input type="text" /&amp;gt;&lt;/code&gt;, we have to programmatically cover these behaviors. The &lt;code&gt;spinbutton&lt;/code&gt; element with type text generally comes with two buttons (one for decrement and the other to increment the value) that should be excluded from the navigation flow by adding &lt;code&gt;tabIndex={-1}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type="text"&lt;/code&gt;: implies that the &lt;code&gt;spinbutton&lt;/code&gt; will receive a value of type text, which is a pain because it means that we have to transform the type text to type number later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inputMode="numeric"&lt;/code&gt;: since the input is type text, we have to make the browsers know which virtual keyboard they will have to display. This is vital for mobile navigation because without the value &lt;u&gt;numeric&lt;/u&gt; the virtual keyboard display to enter a value will be a QWERTY instead of a numeric input keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pattern="[0-9]*"&lt;/code&gt;: again, since the input is &lt;code&gt;type="text"&lt;/code&gt;, we have to use a regex to only admit values of type number. (This attribute was not on the APG example, but I cannot help myself).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autocomplete="off"&lt;/code&gt;: it is used on inputs that take a text or numeric value as input to prevent the browser from automatically entering a value into this field. (again, my take, not on APG example).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-valuemin="[NUMBER]"&lt;/code&gt;: sets the minimum value allowed for the spinbutton. The default is 0, if no value is passed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-valuemax="[NUMBER]"&lt;/code&gt;: sets the maximum value allowed for the spinbutton. The default is 100, if no value is passed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-valuenow="[number]"&lt;/code&gt;: here things start to get interesting. By definition, this attribute defines the current value for a range widget (meter, scrollbar, slider and spinbutton). Since we are not using a semantic solution, this value should be updated programmatically. Also, this value is intended for numeric values and APG provides an example where the &lt;code&gt;spinbutton&lt;/code&gt; is being considered as a text, so we have to also transform the value from type string to type number. This is not everything! Since the &lt;code&gt;spinbutton&lt;/code&gt; is not correctly typed, the announcement of the current value will be done as a percent by VoiceOver (macOS and iOS) announces it as a percent, TalkBack on Pixel 10 does not announce it at all, and NVDA announces it correctly. You can test it on the &lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/examples/quantity-spinbutton/#ex_label" rel="noopener noreferrer"&gt;APG example&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-invalid="[boolean]"&lt;/code&gt;: indicates that the current value is invalid.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby="[IDREF]"&lt;/code&gt;: this attribute is used to establish a relationship between widgets or groups and the text that describes them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Conclusion for the APG Spinbutton Pattern
&lt;/h3&gt;

&lt;p&gt;To articulate a conclusion for this pattern, I drew on &lt;a href="https://www.w3.org/TR/core-aam-1.2/" rel="noopener noreferrer"&gt;Core Accessibility API Mappings 1.2&lt;/a&gt; which is a guide that specifies how WAI-ARIA roles, states, and properties are expected to be exposed by user agents (browsers, in this case) via platform accessibility APIs. &lt;br&gt;&lt;br&gt;
Since a &lt;code&gt;spinbutton&lt;/code&gt; is considered a range widget, it's implicitly expected to receive a value of type number even though the APG pattern tells us otherwise. By implementing a role &lt;code&gt;spinbutton&lt;/code&gt; in a text input, we will be going against the nature of the role, which leads us to do extra work to make it work correctly.&lt;br&gt;
The pattern is good but we can make it better, with some browser limitations (the party can never be enjoyed in peace), with the native HTML solution as we will see below.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern B: Native HTML solution
&lt;/h2&gt;

&lt;p&gt;Unfortunately, nothing is perfect; the native HTML solution for &lt;code&gt;spinbutton&lt;/code&gt; is tempting because it works well in most cases BUT has a few limitations that aren't HTML-related but browser-related, buuuh. &lt;br&gt;&lt;br&gt;
Browsers have different engines to create what we colloquially know as &lt;u&gt;Accessibility Tree&lt;/u&gt;. This tree is just an object with &lt;u&gt;nodes&lt;/u&gt;. Every node includes information about the HTML elements and attributes. Finally, this object is being exposed to the Accessibility APIs of every operating system so they can translate the information and make it readable to assistive technologies. Here is where the things get complicated because every browser engine sometimes interprets things differently. Below, you can find a list of the browser engines for the different browsers and the Accessibility APIs they support.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blink&lt;/strong&gt;: this is the browser engine for &lt;u&gt;Chrome&lt;/u&gt;, &lt;u&gt;Edge&lt;/u&gt;, &lt;u&gt;Opera&lt;/u&gt; and &lt;u&gt;Brave&lt;/u&gt; (all browsers based on Chromium) which means all the browsers are going to have the same Accessibility Tree as the one we can see on the following image:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4cap6mnpryi7wd1wwxb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4cap6mnpryi7wd1wwxb.png" alt="Chrome DevTools accessibility node showing the computed properties for a spinbutton: role spinbutton, name " width="716" height="94"&gt;&lt;/a&gt;&lt;br&gt;
Supported on the following Accessibility APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: IAccessible, IAccessible2 and UIAutomation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mac&lt;/strong&gt;: NSAccessibility (funny thing here is that Blink creates a more accurate and complete node than WebKit and VoiceOver announces the spinbutton better on Chrome, as we are going to read later)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: ATK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt;: AccessibilityNodeInfo and AccessibilityNodeProvider. I opened a ticket for this operating system because it is not exposing the &lt;code&gt;spinbutton&lt;/code&gt; correctly preventing the user from having an accessible experience. The ticket to Android can be seen &lt;a href="https://issues.chromium.org/issues/511602085" rel="noopener noreferrer"&gt;here&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gecko&lt;/strong&gt;: this is the browser engine for &lt;u&gt;Firefox&lt;/u&gt;. The Accessibility Tree it creates, in my opinion, is the most complete and it is compatible with the following Accessibility APIs:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: IAccessible, IAccessible2 and UIAutomation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mac&lt;/strong&gt;: mozAccessible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: ATK

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebKit&lt;/strong&gt;: this is the browser engine for &lt;u&gt;Safari&lt;/u&gt; in macOS and iOS. Here is where our problems start. The engine does not recognize the element as a &lt;code&gt;spinbutton&lt;/code&gt; but as a &lt;code&gt;role="textbox"&lt;/code&gt; if we don't explicitly pass the &lt;code&gt;role="spinbutton"&lt;/code&gt; to the &lt;code&gt;&amp;lt;input type="text" /&amp;gt;&lt;/code&gt;. This is an issue because the VoiceOver screen reader in both devices, desktop and mobile, will prevent the user from having an accessible experience, the same issue that I faced on Android.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1q2kez40al3kgwddta6t.png" alt="'Safari Web Inspector accessibility panel showing: Role textbox (default), Label " width="436" height="326"&gt;
Again, I ended up opening a ticket to Apple about this behavior. The ticket to Apple can be seen &lt;a href="https://developer.apple.com/forums/thread/825940" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Functional Example – HTML
&lt;/h3&gt;

&lt;p&gt;The native functional example can be found &lt;a href="https://micaavigliano.com/en/blog/quantity-spinbutton-pattern#functional-example-html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  HTML native code
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"add-to-bag-native"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"spinbutton"&lt;/span&gt;
  &lt;span class="na"&gt;autoComplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;
  &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;{MIN}&lt;/span&gt;
  &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;{MAX}&lt;/span&gt;
  &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;{STEP}&lt;/span&gt;
  &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;{value}&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt;: same thing as in the APG pattern&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type="number"&lt;/code&gt;: this is what natively converts an input into a &lt;code&gt;spinbutton&lt;/code&gt; and restricts it to numeric values. It natively supports supports increasing and decreasing the value by: pressing the native controls that come with the input &lt;code&gt;type="number"&lt;/code&gt; (see/read/perceive the image below), pressing Up Arrow or Down Arrow, or swiping (on mobile with the screen readers on).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdrioi2uty3leg9dxuod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdrioi2uty3leg9dxuod.png" alt="A spinbutton with a single value with controls to increase or decrease a value. Default appearance and accessibility behavior vary substantially across browser and platform combinations" width="148" height="138"&gt;&lt;/a&gt;&lt;br&gt;
  The appearance of these arrows inside the input comes by default and we can override it if we provide another mechanism to substitute the buttons themselves; we already did that with the custom buttons to increase and decrease the value, so we are good to go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"number"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;appearance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;textfield&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"number"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-outer-spin-button&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"number"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-inner-spin-button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;-webkit-appearance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;appearance: textfield;&lt;/code&gt;, we need it to strip the native spinbutton styling in Firefox&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;::-webkit-outer-spin-button&lt;/code&gt; and &lt;code&gt;::-webkit-inner-spin-button&lt;/code&gt; are pseudo-selectors that only Blink and WebKit recognize and the &lt;code&gt;-webkit-appearance: none&lt;/code&gt; property strips the native spinbutton styling in Chrome and Safari.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;role="spinbutton"&lt;/code&gt;: well, having in mind all the compatibility issues we have in different browsers, I found out that if you &lt;strong&gt;only&lt;/strong&gt; pass the WAI-ARIA &lt;code&gt;role="spinbutton"&lt;/code&gt; magically they all disappear. So, there is no harm in adding it to prevent headaches in the future. This post was built for this moment, you can thank me.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autoComplete="off"&lt;/code&gt;: just the same as in the APG example&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;min={NUMBER}&lt;/code&gt;: the minimum value to accept for this input and should be less than or equal to the maximum value. If a value that isn't a valid number is specified, then the input has no minimum.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max={NUMBER}&lt;/code&gt;: the maximum value to accept for this input and should be greater than or equal to the minimum value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;step={NUMBER}&lt;/code&gt;: defines the allowed interval between valid values. The default is 1 and only accepts integers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;value={NUMBER}&lt;/code&gt;: a number representing the value of the number entered into the input&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion for the HTML native solution
&lt;/h3&gt;

&lt;p&gt;If you want to have a functional, accessible and widely supported across browsers &lt;code&gt;spinbutton&lt;/code&gt; component, I recommend using my solution and to always go to the native solutions and, if you do not feel comfortable enough, talk to your trustworthy accessibility expert. This native example with the workaround of adding a WAI-ARIA &lt;code&gt;role="spinbutton"&lt;/code&gt; to force its semantic value on some browser engines was tested on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome (version 148) + macOS (Tahoe version 26.4.1) + VoiceOver&lt;/li&gt;
&lt;li&gt;Chrome (version 148) + iOS (version 26.4.1)/ iPhone pro 15 + VoiceOver&lt;/li&gt;
&lt;li&gt;Chrome (version 148) + Windows 11 + NVDA&lt;/li&gt;
&lt;li&gt;Chrome (version 148) + Android/Pixel 10 (version 16) + TalkBack&lt;/li&gt;
&lt;li&gt;Firefox (version 150) + Windows 11 + NVDA&lt;/li&gt;
&lt;li&gt;Firefox (version 150) + macOS (Tahoe version 26.4.1)&lt;/li&gt;
&lt;li&gt;Safari (version 26.4) + macOS (Tahoe version 26.4.1) + VoiceOver&lt;/li&gt;
&lt;li&gt;Safari (version 26.4) + iOS (version 26.4.1) + VoiceOver&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I did not cover but it's important to mention: the Home and End keyboard shortcuts mentioned on the Keyboard section are not provided natively so you have to cover them programmatically. Also, some keyboards, such as Apple's keyboards (always Apple) do not have keys for Home and End and they are substituted by the combination Command or or Fn + Right/Left Arrows so you have to cover this combination as well. It's just a function of a 9-line function, nothing too exceptional, you should contemplate it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleKeyDown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;End&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metaKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ArrowRight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metaKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ArrowLeft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Having a semantic, functional and accessible &lt;code&gt;spinbutton&lt;/code&gt; on your e-commerce will not only contribute to improving the overall internet experience to everyone but it will leverage your product and increase your sales. A win-win for all parties involved; you get the money, the user, no matter how he/she/they are navigating, gets the product.&lt;br&gt;
According to &lt;a href="https://www.who.int/news-room/fact-sheets/detail/assistive-technology" rel="noopener noreferrer"&gt;WHO&lt;/a&gt; more than 2.5 billion people need one or more assistive products to to meet their daily needs, and one of those needs is the right to navigate freely through the web. You do not really know who is navigating through your website behind the screen, could be a human with different needs or now an AI AGENT (!!!!). Omg, a new party entered the game and should be contemplated too. Nowadays, some users are relying on autonomous purchase bots, voice shopping assistants or AI browsing agents. These AI assistants base their navigation on the web in the same way as any other assistive technology: they consume the data provided by the Accessibility Tree. Amazing. &lt;br&gt;&lt;br&gt;
Well, we learned that: a new player entered the game (AI assistants), the native HTML solution is always the best option (might be buggy but it's the browser's fault) and you should always be critical and test everything you are building.&lt;br&gt;&lt;/p&gt;

&lt;p&gt;You can find me on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email: &lt;a href="//micaela.avigliano@gmail.com"&gt;micaela.avigliano@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;linkedin: &lt;a href="https://www.linkedin.com/in/micaelaavigliano/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/micaelaavigliano/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>designsystem</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>HTML tags that will improve your e-commerce experience</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Tue, 01 Sep 2026 09:42:58 +0000</pubDate>
      <link>https://dev.to/micaavigliano/html-tags-that-will-improve-your-e-commerce-experience-1lmi</link>
      <guid>https://dev.to/micaavigliano/html-tags-that-will-improve-your-e-commerce-experience-1lmi</guid>
      <description>&lt;h2&gt;
  
  
  Understanding when to use &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; HTML tags
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparative Feature&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic Definition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Represents the content that has been added to a document.&lt;/td&gt;
&lt;td&gt;Represents a range of text that has been deleted from a document.&lt;/td&gt;
&lt;td&gt;Represents content that is no longer accurate, correct, or relevant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;New edits in a code, in a text, tracked changes&lt;/td&gt;
&lt;td&gt;Document edits, tracked changes, or visual/structural revisions (often paired with &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;Outdated information, deprecation notices, old prices, or sold-out items.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accessible Code Pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;The meeting is on &amp;lt;span class="sr-only"&amp;gt;previous date: &amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;Monday&amp;lt;/del&amp;gt; &amp;lt;span class="sr-only"&amp;gt;new date: &amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;Wednesday&amp;lt;/ins&amp;gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;The meeting is on &amp;lt;span class="sr-only"&amp;gt;previous date: &amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;Monday&amp;lt;/del&amp;gt; &amp;lt;span class="sr-only"&amp;gt;new date: &amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;Wednesday&amp;lt;/ins&amp;gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;span class="sr-only"&amp;gt;Original price: &amp;lt;/span&amp;gt;&amp;lt;s&amp;gt;$100.00&amp;lt;/s&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visible Representation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The meeting is on &lt;del&gt;Monday&lt;/del&gt; Wednesday&lt;/td&gt;
&lt;td&gt;The meeting is on &lt;del&gt;Monday&lt;/del&gt; Wednesday&lt;/td&gt;
&lt;td&gt;
&lt;del&gt;$100.00&lt;/del&gt; &lt;span&gt;$34.99&lt;/span&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unique Attributes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cite&lt;/code&gt; (URL pointing to the explanation of the deletion)&lt;br&gt;&lt;code&gt;datetime&lt;/code&gt; (date/time of the deletion)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cite&lt;/code&gt; (URL pointing to the explanation of the deletion)&lt;br&gt;&lt;code&gt;datetime&lt;/code&gt; (date/time of the deletion)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default Browser Style&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;By default, it has an underline but it can be changed to a bold style, put a background green to show insertion, etc.&lt;/td&gt;
&lt;td&gt;Renders with a visual line-through (strikethrough)&lt;/td&gt;
&lt;td&gt;Renders with a visual line-through (strikethrough)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Implicit ARIA Mapping: role="deletion" and role="insertion"
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; tags map to the accessibility role of &lt;code&gt;deletion&lt;/code&gt; (and &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; to &lt;code&gt;insertion&lt;/code&gt;). Sighted users see these as struck through or underlined, but screen reader support for announcing these changes is inconsistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the Accessibility Tree Mapping
&lt;/h3&gt;

&lt;p&gt;Under the W3C Accessibility API Mappings, these tags are programmatically mapped to specific accessibility roles that browsers expose to the OS accessibility tree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="deletion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-del" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-deletion" rel="noopener noreferrer"&gt;Core-AAM deletion role map&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="deletion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-s" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-deletion" rel="noopener noreferrer"&gt;Core-AAM deletion role map&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="insertion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-ins" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-insertion" rel="noopener noreferrer"&gt;Core-AAM insertion role map&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These mappings instruct the browser's accessibility engine on how to expose the semantics to assistive technology. However, screen reader support is inconsistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Natively Announced:&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;VoiceOver + Safari (iOS)&lt;/li&gt;
&lt;li&gt;NVDA + Firefox&lt;/li&gt;
&lt;li&gt;TalkBack + Chrome&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not Announced:&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;NVDA + Chrome&lt;/li&gt;
&lt;li&gt;TalkBack + Firefox&lt;/li&gt;
&lt;li&gt;VoiceOver + Safari (macOS)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How we provide context for combinations without native support on the Accessibility Tree
&lt;/h3&gt;

&lt;p&gt;To make up for the lack of support, we should add a visually hidden text (e.g., using a &lt;code&gt;.sr-only&lt;/code&gt; class) to provide more context about what is happening with the information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;The meeting is on &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;previous date: &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;&lt;/span&gt;Monday&lt;span class="nt"&gt;&amp;lt;/del&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;new date: &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;&lt;/span&gt;Wednesday&lt;span class="nt"&gt;&amp;lt;/ins&amp;gt;&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By framing the visually hidden text as natural contextual descriptors (e.g., &lt;em&gt;"previous date:"&lt;/em&gt; and &lt;em&gt;"new date:"&lt;/em&gt;), the output will be more coherent and meaningful across all assistive technologies. It is recommended to avoid redundant labels like &lt;em&gt;"deletion"&lt;/em&gt; and &lt;em&gt;"insertion"&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On unsupported systems:&lt;/strong&gt; Announces &lt;em&gt;"previous date: Monday, new date: Wednesday"&lt;/em&gt;. The semantic change is fully communicated through the descriptive context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On supported systems:&lt;/strong&gt; Announces &lt;em&gt;"previous date: deletion, Monday, new date: insertion, Wednesday"&lt;/em&gt;. This reads as a coherent, descriptive sentence without redundant verbal stuttering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CSS Styling Tips
&lt;/h2&gt;

&lt;p&gt;You can customize the visual representation of &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; tags to match your design system using standard CSS text-decoration properties, as well as define a utility class to hide screen-reader context labels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To style deletions and strikethroughs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Customize individually */&lt;/span&gt;
&lt;span class="nt"&gt;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;del&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;line-through&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e06c75&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* Change line color */&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* double, dashed, wavy, solid */&lt;/span&gt;
  &lt;span class="py"&gt;text-decoration-thickness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* Change line thickness */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Or use the shorthand syntax */&lt;/span&gt;
&lt;span class="nt"&gt;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;del&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;line-through&lt;/span&gt; &lt;span class="m"&gt;#e06c75&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customizing &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To style insertions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Customize individually */&lt;/span&gt;
&lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;underline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#98c379&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* Change line color */&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* double, dashed, wavy, solid */&lt;/span&gt;
  &lt;span class="py"&gt;text-decoration-thickness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* Change line thickness */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Customize addition of code */&lt;/span&gt;
&lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3fb950&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Or use the shorthand syntax */&lt;/span&gt;
&lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;underline&lt;/span&gt; &lt;span class="m"&gt;#98c379&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Visual Hiding Utility
&lt;/h3&gt;

&lt;p&gt;To hide descriptive contextual labels (like &lt;code&gt;"previous date:"&lt;/code&gt;) from visual users while keeping them accessible to screen readers, implement a standard visually-hidden CSS class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.sr-only&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resources and Documentation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/del" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ins" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/s" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Clarification on MDN's Pseudo-Element Solution:&lt;/strong&gt;&lt;br&gt;
MDN suggests using CSS generated content (&lt;code&gt;::before&lt;/code&gt; / &lt;code&gt;::after&lt;/code&gt;) to announce which text was deleted and which text was inserted. However, I opted for another also valid solution. According to my tests, using visually hidden DOM nodes is much more reliable and stable to ensure a clear context.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>html</category>
      <category>seo</category>
    </item>
    <item>
      <title>Master price tags</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Mon, 17 Aug 2026 20:02:48 +0000</pubDate>
      <link>https://dev.to/micaavigliano/master-price-tags-167d</link>
      <guid>https://dev.to/micaavigliano/master-price-tags-167d</guid>
      <description>&lt;h2&gt;
  
  
  Understanding when to use &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; HTML tags
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparative Feature&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic Definition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Represents the content that has been added to a document.&lt;/td&gt;
&lt;td&gt;Represents a range of text that has been deleted from a document.&lt;/td&gt;
&lt;td&gt;Represents content that is no longer accurate, correct, or relevant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;New edits in a code, in a text, tracked changes&lt;/td&gt;
&lt;td&gt;Document edits, tracked changes, or visual/structural revisions (often paired with &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;Outdated information, deprecation notices, old prices, or sold-out items.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accessible Code Pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;The meeting is on &amp;lt;span class="sr-only"&amp;gt;previous date: &amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;Monday&amp;lt;/del&amp;gt; &amp;lt;span class="sr-only"&amp;gt;new date: &amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;Wednesday&amp;lt;/ins&amp;gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;The meeting is on &amp;lt;span class="sr-only"&amp;gt;previous date: &amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;Monday&amp;lt;/del&amp;gt; &amp;lt;span class="sr-only"&amp;gt;new date: &amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;Wednesday&amp;lt;/ins&amp;gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;span class="sr-only"&amp;gt;Original price: &amp;lt;/span&amp;gt;&amp;lt;s&amp;gt;$100.00&amp;lt;/s&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visible Representation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The meeting is on &lt;del&gt;Monday&lt;/del&gt; Wednesday&lt;/td&gt;
&lt;td&gt;The meeting is on &lt;del&gt;Monday&lt;/del&gt; Wednesday&lt;/td&gt;
&lt;td&gt;
&lt;del&gt;$100.00&lt;/del&gt; &lt;span&gt;$34.99&lt;/span&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unique Attributes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cite&lt;/code&gt; (URL pointing to the explanation of the deletion)&lt;br&gt;&lt;code&gt;datetime&lt;/code&gt; (date/time of the deletion)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cite&lt;/code&gt; (URL pointing to the explanation of the deletion)&lt;br&gt;&lt;code&gt;datetime&lt;/code&gt; (date/time of the deletion)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default Browser Style&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;By default, it has an underline but it can be changed to a bold style, put a background green to show insertion, etc.&lt;/td&gt;
&lt;td&gt;Renders with a visual line-through (strikethrough)&lt;/td&gt;
&lt;td&gt;Renders with a visual line-through (strikethrough)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Implicit ARIA Mapping: role="deletion" and role="insertion"
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; tags map to the accessibility role of &lt;code&gt;deletion&lt;/code&gt; (and &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; to &lt;code&gt;insertion&lt;/code&gt;). Sighted users see these as struck through or underlined, but screen reader support for announcing these changes is inconsistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the Accessibility Tree Mapping
&lt;/h3&gt;

&lt;p&gt;Under the W3C Accessibility API Mappings, these tags are programmatically mapped to specific accessibility roles that browsers expose to the OS accessibility tree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="deletion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-del" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-deletion" rel="noopener noreferrer"&gt;Core-AAM deletion role map&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="deletion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-s" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-deletion" rel="noopener noreferrer"&gt;Core-AAM deletion role map&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="insertion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-ins" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-insertion" rel="noopener noreferrer"&gt;Core-AAM insertion role map&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These mappings instruct the browser's accessibility engine on how to expose the semantics to assistive technology. However, screen reader support is inconsistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Natively Announced:&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;VoiceOver + Safari (iOS)&lt;/li&gt;
&lt;li&gt;NVDA + Firefox&lt;/li&gt;
&lt;li&gt;TalkBack + Chrome&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not Announced:&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;NVDA + Chrome&lt;/li&gt;
&lt;li&gt;TalkBack + Firefox&lt;/li&gt;
&lt;li&gt;VoiceOver + Safari (macOS)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How we provide context for combinations without native support on the Accessibility Tree
&lt;/h3&gt;

&lt;p&gt;To make up for the lack of support, we should add a visually hidden text (e.g., using a &lt;code&gt;.sr-only&lt;/code&gt; class) to provide more context about what is happening with the information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;The meeting is on &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;previous date: &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;&lt;/span&gt;Monday&lt;span class="nt"&gt;&amp;lt;/del&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;new date: &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;&lt;/span&gt;Wednesday&lt;span class="nt"&gt;&amp;lt;/ins&amp;gt;&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By framing the visually hidden text as natural contextual descriptors (e.g., &lt;em&gt;"previous date:"&lt;/em&gt; and &lt;em&gt;"new date:"&lt;/em&gt;), the output will be more coherent and meaningful across all assistive technologies. It is recommended to avoid redundant labels like &lt;em&gt;"deletion"&lt;/em&gt; and &lt;em&gt;"insertion"&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On unsupported systems:&lt;/strong&gt; Announces &lt;em&gt;"previous date: Monday, new date: Wednesday"&lt;/em&gt;. The semantic change is fully communicated through the descriptive context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On supported systems:&lt;/strong&gt; Announces &lt;em&gt;"previous date: deletion, Monday, new date: insertion, Wednesday"&lt;/em&gt;. This reads as a coherent, descriptive sentence without redundant verbal stuttering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CSS Styling Tips
&lt;/h2&gt;

&lt;p&gt;You can customize the visual representation of &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; tags to match your design system using standard CSS text-decoration properties, as well as define a utility class to hide screen-reader context labels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To style deletions and strikethroughs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Customize individually */&lt;/span&gt;
&lt;span class="nt"&gt;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;del&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;line-through&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e06c75&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* Change line color */&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* double, dashed, wavy, solid */&lt;/span&gt;
  &lt;span class="py"&gt;text-decoration-thickness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* Change line thickness */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Or use the shorthand syntax */&lt;/span&gt;
&lt;span class="nt"&gt;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;del&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;line-through&lt;/span&gt; &lt;span class="m"&gt;#e06c75&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customizing &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To style insertions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Customize individually */&lt;/span&gt;
&lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;underline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#98c379&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* Change line color */&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* double, dashed, wavy, solid */&lt;/span&gt;
  &lt;span class="py"&gt;text-decoration-thickness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* Change line thickness */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Customize addition of code */&lt;/span&gt;
&lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3fb950&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Or use the shorthand syntax */&lt;/span&gt;
&lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;underline&lt;/span&gt; &lt;span class="m"&gt;#98c379&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Visual Hiding Utility
&lt;/h3&gt;

&lt;p&gt;To hide descriptive contextual labels (like &lt;code&gt;"previous date:"&lt;/code&gt;) from visual users while keeping them accessible to screen readers, implement a standard visually-hidden CSS class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.sr-only&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resources and Documentation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/del" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ins" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/s" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Clarification on MDN's Pseudo-Element Solution:&lt;/strong&gt;&lt;br&gt;
MDN suggests using CSS generated content (&lt;code&gt;::before&lt;/code&gt; / &lt;code&gt;::after&lt;/code&gt;) to announce which text was deleted and which text was inserted. However, I opted for another also valid solution. According to my tests, using visually hidden DOM nodes is much more reliable and stable to ensure a clear context.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>designsystem</category>
      <category>a11y</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Learn how to build a disclosure component using the native HTML details tag</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Mon, 17 Aug 2026 13:08:06 +0000</pubDate>
      <link>https://dev.to/micaavigliano/learn-how-to-build-an-expandable-and-collapsible-component-using-the-native-html-details-tag-1h8j</link>
      <guid>https://dev.to/micaavigliano/learn-how-to-build-an-expandable-and-collapsible-component-using-the-native-html-details-tag-1h8j</guid>
      <description>&lt;p&gt;Welcome to a new entry in my section dedicated to creating &lt;strong&gt;reusable&lt;/strong&gt; and, above all, &lt;strong&gt;accessible&lt;/strong&gt; components. This time, we are going to learn how to use the HTML &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag to create a disclosure component without the need for JavaScript or WAI-ARIA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements to have a functional disclosure
&lt;/h2&gt;

&lt;p&gt;To make a disclosure accessible, a few requirements must be met:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the focus is on the disclosure element header &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;, it must be possible to collapse and expand it by pressing the Enter or Space keys.&lt;/li&gt;
&lt;li&gt;When pressing Tab, the focus moves to the next interactive element inside the disclosure element or, if there are none, to the next interactive element on the page.&lt;/li&gt;
&lt;li&gt;When pressing Shift + Tab, the focus moves to the previous interactive element.&lt;/li&gt;
&lt;li&gt;The screen reader must announce the state of the element, that is, whether it is collapsed or expanded. It must also announce its accessible name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag will help us meet all these points, since it is natively accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of the details tag
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;/strong&gt;&lt;strong&gt;&lt;/strong&gt;: The main element that wraps the whole disclosure element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;/strong&gt;&lt;strong&gt;&lt;/strong&gt;: First direct child of the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag. It is used as a header acting as the control to &lt;u&gt;expand&lt;/u&gt; or &lt;u&gt;collapse&lt;/u&gt; the content. There is no need to group the content of the summary inside another tag, but the following tags are valid to use: &lt;code&gt;&amp;lt;h1&amp;gt;-&amp;lt;h6&amp;gt;&lt;/code&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Content_categories#phrasing_content" rel="noopener noreferrer"&gt;phrasing content&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;::marker&lt;/strong&gt;: The disclosure triangle that indicates whether the element is open or closed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;::details-content&lt;/strong&gt;: Direct child of the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag and sibling of &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; and it can be any valid HTML element. The best part is that there is no need to wrap the content inside a div or a fragment, natively the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag will detect it as a content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Attributes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;open&lt;/strong&gt;: This attribute indicates whether the details content is expanded or collapsed.

&lt;ul&gt;
&lt;li&gt;This attribute is implicit on the tag, so there is no need to add it because the details tag is collapsed by default.&lt;/li&gt;
&lt;li&gt;If a plain &lt;code&gt;open&lt;/code&gt; is added, the details will be expanded by default.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;name&lt;/strong&gt;: This attribute will only be necessary when it is needed to manage more than one details element as a group. In other words, if one details is open and another is opened, the previously open one will automatically close.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CSS Strategies to Style The Disclosure Component
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How to style summary
&lt;/h3&gt;

&lt;p&gt;Natively, the &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; tag comes by default with the property &lt;code&gt;display: list-item&lt;/code&gt;. This property gives us the native the disclosure icon, which can be a triangle, a circle, only the disclosure-open or the disclosure-close icon, or use the CSS at-rule &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@counter-style" rel="noopener noreferrer"&gt;@counter-style&lt;/a&gt; to set a predefined list of icon styles. On the other hand, with the pseudo-element &lt;code&gt;::marker&lt;/code&gt;, the disclosure icon can be styled:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;u&gt;::marker – Code Snippet&lt;/u&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;::marker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;orangered&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;u&gt;::marker – Functional Example&lt;/u&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/9hpg6m"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Cool, but now, how can the native disclosure icon be omitted? Since the &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; tag comes by default with the property &lt;code&gt;display: list-item&lt;/code&gt;, by simply adding a &lt;code&gt;display: flex&lt;/code&gt; on the &lt;code&gt;details &amp;gt; summary&lt;/code&gt; selector, the native disclosure icon is hidden automatically. This strategy is valid, but it is necessary to have a visual symbol that makes it explicit that this is a disclosure element. The added icon should be &lt;code&gt;aria-hidden="true"&lt;/code&gt; because assistive technology users will already perceive the role and the state of the element.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;u&gt;Removing Native Disclosure Icon – Code Snippet&lt;/u&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;u&gt;Removing Native Disclosure Icon – Functional Example&lt;/u&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/lgqc69"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Finally, I would like to comment that there is also another well-known pseudo-element named &lt;code&gt;::-webkit-details-marker&lt;/code&gt; that is used in WebKit-based browsers to style details marker. To be honest, that pseudo-selector today is not really necessary since &lt;code&gt;::marker&lt;/code&gt; is well supported across different browsers. &lt;/p&gt;

&lt;h3&gt;
  
  
  Bye, bye div to generate content!
&lt;/h3&gt;

&lt;p&gt;Since September 2025, we have the CSS pseudo-selector &lt;code&gt;::details-content&lt;/code&gt; that auto-generates a wrapper box to style the expandable/collapsible content of the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; tag. This is good news because now we do not need to rely on a non-semantic tag, like a &lt;u&gt;div&lt;/u&gt;, to style the wrapper of the content and to handle the transitions from the collapsed state to the expanded and vice versa.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;u&gt;Expandable/Collapsible Content – CSS Snippet&lt;/u&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nd"&gt;::details-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;::details-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.85rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;no-preference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;details&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;details-content&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;block-size&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;padding-block&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;content-visibility&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt; &lt;span class="n"&gt;allow-discrete&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;details::details-content&lt;/code&gt;, holds the styles for the collapsed default state and hides the content.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;details[open]::details-content&lt;/code&gt;, holds the styles for the expanded state. It will determine the end of the animation, if there is any.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@media (prefers-reduced-motion: no-preference) {details::details-content {}}&lt;/code&gt;, &lt;u&gt;this is an accessibility improvement!&lt;/u&gt; Here is where the animation between the closed and open states is going to live if the user has animations enabled in their operating system.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;u&gt;Expandable/Collapsible Content – Functional Example&lt;/u&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/9lqjjl"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A correct semantic structure would be rendered as the following block of code, and you will not find any non-semantic HTML element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;{...}&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  {here starts the ::details-content}
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{...}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h4&amp;gt;&lt;/span&gt;{...}&lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{...}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{...}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{...}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keyboard Navigation
&lt;/h2&gt;

&lt;p&gt;&lt;u&gt;The element that receives focus and manages the interaction is &lt;code&gt;summary&lt;/code&gt;&lt;/u&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tab&lt;/td&gt;
&lt;td&gt;Move focus to the disclosure element or move focus to the next disclosure element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Shift + Tab
&lt;/td&gt;
&lt;td&gt;Move focus to the previous disclosure element or move focus to the previous interactive element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Space or Enter
&lt;/td&gt;
&lt;td&gt;Expands/collapses the details element&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Screen Readers
&lt;/h2&gt;

&lt;p&gt;Here is how different combinations of browsers, devices and screen readers announce and interact with a disclosure element:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;u&gt;VoiceOver macOS Tahoe 26.5.2 + Safari 26.5.2 (21624.2.5.11.8)&lt;/u&gt;:

&lt;ul&gt;
&lt;li&gt;Collapsed: 'How do screen readers announce the details tag?, collapsed, summary'&lt;/li&gt;
&lt;li&gt;Expanded: 'How do screen readers announce the details tag?, expanded, summary'&lt;/li&gt;
&lt;li&gt;Toggle state with keys combination Control + Option + Space: 'collapsed' or 'expanded'&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;VoiceOver macOS Tahoe 26.5.2 + Chrome 150.0.7871.184&lt;/u&gt;:

&lt;ul&gt;
&lt;li&gt;Collapsed: 'How do screen readers announce the details tag?, collapsed, disclosure triangle, group'&lt;/li&gt;
&lt;li&gt;Expanded: 'How do screen readers announce the details tag?, expanded, disclosure triangle, group'&lt;/li&gt;
&lt;li&gt;Toggle state with keys combination Control + Option + Space: if it is collapsed, the whole thing again is announced as collapsed and if it is expanded, the whole thing again is announced as expanded&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;VoiceOver macOS Tahoe 26.5.2 + Firefox 152.0.6&lt;/u&gt;:

&lt;ul&gt;
&lt;li&gt;same as in the combination VoiceOver macOS Tahoe 26.5.2 + Chrome 150.0.7871.184&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;NVDA 2026.1.1 + Chrome 150.0.7871.18&lt;/u&gt;:

&lt;ul&gt;
&lt;li&gt;Collapsed: 'How do screen readers announce the details tag?, button, collapsed'&lt;/li&gt;
&lt;li&gt;Expanded: 'How do screen readers announce the details tag?, button, expanded'&lt;/li&gt;
&lt;li&gt;Toggle state with Enter or Space: 'collapsed' or 'expanded'&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;NVDA 2026.1.1 + Firefox 153.0&lt;/u&gt;:

&lt;ul&gt;
&lt;li&gt;Collapsed: 'How do screen readers announce the details tag?, button, collapsed'&lt;/li&gt;
&lt;li&gt;Expanded: 'How do screen readers announce the details tag?, button, expanded'&lt;/li&gt;
&lt;li&gt;Toggle state with Enter or Space: 'collapsed' or 'expanded'&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;TalkBack, Pixel 10, Android 16 + Chrome 149.0.7827.160&lt;/u&gt;:

&lt;ul&gt;
&lt;li&gt;Collapsed: 'collapsed, How do screen readers announce the details tag?, disclosure triangle'&lt;/li&gt;
&lt;li&gt;Expanded: 'expanded, How do screen readers announce the details tag?, disclosure triangle'&lt;/li&gt;
&lt;li&gt;Toggle state by double-tapping to activate: 'collapsed' or 'expanded'&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;TalkBack, Pixel 10, Android 16 + Firefox 145.0.2&lt;/u&gt;:

&lt;ul&gt;
&lt;li&gt;Collapsed: 'collapsed, How do screen readers announce the details tag?, button, How do screen readers announce the details tag?, Space'&lt;/li&gt;
&lt;li&gt;Expanded: 'expanded, How do screen readers announce the details tag?, button, How do screen readers announce the details tag?, Space'&lt;/li&gt;
&lt;li&gt;Toggle state by double-tapping to activate: 'collapsed' or 'expanded'&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;p&gt;The same element powers very different UI. Here are a few patterns built with nothing but its native tags, the &lt;code&gt;name&lt;/code&gt; attribute and the &lt;code&gt;::details-content&lt;/code&gt; pseudo-element, still with zero JavaScript.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;FAQ section&lt;/strong&gt;: One answer open at a time. The same name attribute on every panel makes the browser close the others for you. &lt;iframe src="https://codesandbox.io/embed/n4t52h"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline "read more"&lt;/strong&gt;: Keep the intro, hide the detail. display: inline on the details and &lt;code&gt;::details-content&lt;/code&gt; folds it into the sentence. &lt;iframe src="https://codesandbox.io/embed/kjhxfj"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spoiler or show solution&lt;/strong&gt;: Hide an answer, a code solution, or a plot spoiler behind an explicit reveal. Closed, the content stays out of view and out of the accessibility tree. &lt;iframe src="https://codesandbox.io/embed/rzdsny"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show order details&lt;/strong&gt;: Keep a card design that has the specifics behind a toggle. Perfect for order metadata, technical specs, or anything power-users want but everyone else can skip. &lt;iframe src="https://codesandbox.io/embed/nc8p8v"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animated markers&lt;/strong&gt;: Because the open/close state is a plain CSS selector, you can animate any marker you like: a chevron that spins, a dot that turns into a ring. &lt;iframe src="https://codesandbox.io/embed/tpl7v9"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collapsible filter panel&lt;/strong&gt;: Group form controls under collapsible headers to keep a long filter rail scannable. Each group remembers its own state; the inputs inside work exactly as they would anywhere else. &lt;iframe src="https://codesandbox.io/embed/js2fvf"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specifications&lt;/strong&gt;: Keep the buy box clean and file the full spec sheet behind a toggle. Shoppers who need dimensions, materials, or care instructions open it. &lt;iframe src="https://codesandbox.io/embed/xwmdpk"&gt;
&lt;/iframe&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collapsed code and logs&lt;/strong&gt;: The GitHub pattern: fold a long code sample, diff, or stack trace behind a summary so issues and docs stay scannable. The revealed block is a monospace panel. &lt;iframe src="https://codesandbox.io/embed/m4d5dg"&gt;
&lt;/iframe&gt;
. This use case uses the tag &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; for the insertion of the new lines. You can read more about this tag its use &lt;a href="https://micaavigliano.com/en/blog/semantic-ins-del-s-tags" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks for reading and leave your comments :)&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>html</category>
      <category>designsystem</category>
    </item>
    <item>
      <title>What is going on?</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:01:35 +0000</pubDate>
      <link>https://dev.to/micaavigliano/what-is-going-on-fho</link>
      <guid>https://dev.to/micaavigliano/what-is-going-on-fho</guid>
      <description>&lt;p&gt;&lt;strong&gt;Playing the game of writing technical post was funny two years ago.&lt;/strong&gt; Now, I am a mod and it is incredible the amount of posts generated by AI that I have to review. Personally, I enjoy reading posts written by humans because it is fun to discover their tone and style, it is fun to read the titles, it is fun to read their experience without having to deal with "it is not A, it is B". JUST STOP. If you do not have anything to share, do not share ANYTHING. We do not need quantity, we need quality. &lt;br&gt;
I want messy posts. I want to know that behind the screen there is a human investing their time typing as in the old days. It does not matter if English is not your first language, just write, just read, just share and be wrong. By making mistakes, you learn. &lt;br&gt;
Finally, I want to share my concerns about the accessibility web community because it worries me how pages like dev.to, smashing magazine, reddit etc, etc have become a sea of ai-generated "accessible" libraries or software to detect automated issues. What is going on? The situation is insufferable.&lt;/p&gt;

&lt;p&gt;Where the humans that generate accessibility content gather today? I do not want to engage with bots anymore.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>a11y</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to write an accessible price tag for e-commerces</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Fri, 26 Jun 2026 06:29:10 +0000</pubDate>
      <link>https://dev.to/micaavigliano/how-to-write-an-accessible-price-tag-for-e-commerces-3gdi</link>
      <guid>https://dev.to/micaavigliano/how-to-write-an-accessible-price-tag-for-e-commerces-3gdi</guid>
      <description>&lt;h2&gt;
  
  
  Understanding when to use &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; HTML tags
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparative Feature&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; element&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic Definition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Represents a range of text that has been deleted from a document.&lt;/td&gt;
&lt;td&gt;Represents content that is no longer accurate, correct, or relevant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Document edits, tracked changes, or visual/structural revisions (often paired with &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;Outdated information, deprecation notices, old prices, or sold-out items.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accessible Code Pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;The meeting is on &amp;lt;span class="sr-only"&amp;gt;previous date: &amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;Monday&amp;lt;/del&amp;gt; &amp;lt;span class="sr-only"&amp;gt;new date: &amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;Wednesday&amp;lt;/ins&amp;gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;span class="sr-only"&amp;gt;Original price: &amp;lt;/span&amp;gt;&amp;lt;s&amp;gt;$100.00&amp;lt;/s&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visible Representation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The meeting is on &lt;del&gt;Monday&lt;/del&gt; Wednesday&lt;/td&gt;
&lt;td&gt;
&lt;del&gt;$100.00&lt;/del&gt; &lt;span&gt;$34.99&lt;/span&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unique Attributes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cite&lt;/code&gt; (URL pointing to the explanation of the deletion)&lt;br&gt;&lt;code&gt;datetime&lt;/code&gt; (date/time of the deletion)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default Browser Style&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Renders with a visual line-through (strikethrough)&lt;/td&gt;
&lt;td&gt;Renders with a visual line-through (strikethrough)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Implicit ARIA Mapping: role="deletion" and role="insertion"
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; tags map to the accessibility role of &lt;code&gt;deletion&lt;/code&gt; (and &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; to &lt;code&gt;insertion&lt;/code&gt;). Sighted users see these as struck through or underlined, but screen reader support for announcing these changes is inconsistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the Accessibility Tree Mapping
&lt;/h3&gt;

&lt;p&gt;Under the W3C Accessibility API Mappings, these tags are programmatically mapped to specific accessibility roles that browsers expose to the OS accessibility tree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="deletion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-del" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-deletion" rel="noopener noreferrer"&gt;Core-AAM deletion role map&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="deletion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-s" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-deletion" rel="noopener noreferrer"&gt;Core-AAM deletion role map&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;&lt;/strong&gt; maps to &lt;code&gt;role="insertion"&lt;/code&gt; (see &lt;a href="https://www.w3.org/TR/html-aam-1.0/#el-ins" rel="noopener noreferrer"&gt;HTML-AAM &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; mapping&lt;/a&gt; and &lt;a href="https://www.w3.org/TR/core-aam-1.2/#role-map-insertion" rel="noopener noreferrer"&gt;Core-AAM insertion role map&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These mappings instruct the browser's accessibility engine on how to expose the semantics to assistive technology. However, screen reader support is inconsistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Natively Announced:&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;VoiceOver + Safari (iOS)&lt;/li&gt;
&lt;li&gt;NVDA + Firefox&lt;/li&gt;
&lt;li&gt;TalkBack + Chrome&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not Announced:&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;NVDA + Chrome&lt;/li&gt;
&lt;li&gt;TalkBack + Firefox&lt;/li&gt;
&lt;li&gt;VoiceOver + Safari (macOS)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How we provide context for combinations without native support on the Accessibility Tree
&lt;/h3&gt;

&lt;p&gt;To make up for the lack of support, we should add a visually hidden text (e.g., using a &lt;code&gt;.sr-only&lt;/code&gt; class) to provide more context about what is happening with the information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;The meeting is on &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;previous date: &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;del&amp;gt;&lt;/span&gt;Monday&lt;span class="nt"&gt;&amp;lt;/del&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;new date: &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;ins&amp;gt;&lt;/span&gt;Wednesday&lt;span class="nt"&gt;&amp;lt;/ins&amp;gt;&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By framing the visually hidden text as natural contextual descriptors (e.g., &lt;em&gt;"previous date:"&lt;/em&gt; and &lt;em&gt;"new date:"&lt;/em&gt;), the output will be more coherent and meaningful across all assistive technologies. It is recommended to avoid redundant labels like &lt;em&gt;"deletion"&lt;/em&gt; and &lt;em&gt;"insertion"&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On unsupported systems:&lt;/strong&gt; Announces &lt;em&gt;"previous date: Monday, new date: Wednesday"&lt;/em&gt;. The semantic change is fully communicated through the descriptive context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On supported systems:&lt;/strong&gt; Announces &lt;em&gt;"previous date: deletion, Monday, new date: insertion, Wednesday"&lt;/em&gt;. This reads as a coherent, descriptive sentence without redundant verbal stuttering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CSS Styling Tips
&lt;/h2&gt;

&lt;p&gt;You can customize the visual representation of &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; tags to match your design system using standard CSS text-decoration properties, as well as define a utility class to hide screen-reader context labels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To style deletions and strikethroughs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="c"&gt;/* Customize individually */&lt;/span&gt;
  &lt;span class="nt"&gt;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;del&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration-line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;line-through&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e06c75&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* Change line color */&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c"&gt;/* double, dashed, wavy, solid */&lt;/span&gt;
    &lt;span class="py"&gt;text-decoration-thickness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* Change line thickness */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;/* Or use the shorthand syntax */&lt;/span&gt;
  &lt;span class="nt"&gt;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;del&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;line-through&lt;/span&gt; &lt;span class="m"&gt;#e06c75&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customizing &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To style insertions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="c"&gt;/* Customize individually */&lt;/span&gt;
  &lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration-line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;underline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#98c379&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c"&gt;/* Change line color */&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c"&gt;/* double, dashed, wavy, solid */&lt;/span&gt;
    &lt;span class="py"&gt;text-decoration-thickness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* Change line thickness */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;/* Or use the shorthand syntax */&lt;/span&gt;
  &lt;span class="nt"&gt;ins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;underline&lt;/span&gt; &lt;span class="m"&gt;#98c379&lt;/span&gt; &lt;span class="n"&gt;wavy&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Visual Hiding Utility
&lt;/h3&gt;

&lt;p&gt;To hide descriptive contextual labels (like &lt;code&gt;"previous date:"&lt;/code&gt;) from visual users while keeping them accessible to screen readers, implement a standard visually-hidden CSS class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="nc"&gt;.sr-only&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resources and Documentation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/del" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ins" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;ins&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/s" rel="noopener noreferrer"&gt;MDN Web Docs: &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt; Element&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer on MDN's Pseudo-Element Solution:&lt;/strong&gt;&lt;br&gt;
Clarification on MDN's Pseudo-Element Solution: MDN suggests using CSS generated content (::before / ::after) to announce which text was deleted and which text was inserted. However, I opted for another also valid solution. According to my tests, using visually hidden DOM nodes is much more reliable and stable to ensure a clear context.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>html</category>
    </item>
    <item>
      <title>Link or Button, that is the question.</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:41:07 +0000</pubDate>
      <link>https://dev.to/micaavigliano/link-or-button-that-is-the-question-2n13</link>
      <guid>https://dev.to/micaavigliano/link-or-button-that-is-the-question-2n13</guid>
      <description>&lt;h2&gt;
  
  
  What is a Link?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Definition
&lt;/h3&gt;

&lt;p&gt;A &lt;u&gt;link&lt;/u&gt; is an interactive element that redirects the user to a new location which can be another section inside the current page, modifying the URL with a &lt;strong&gt;#&lt;/strong&gt; parameter, or a new page. It can be used to download a file. Once activated, it takes the user to the URL set in its href. The browser records that navigation in its history, so the user can return to the previous page using the back button.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic
&lt;/h3&gt;

&lt;p&gt;The elements needs the attribute &lt;u&gt;href&lt;/u&gt; with a valid &lt;u&gt;URL&lt;/u&gt; or an &lt;u&gt;IDREF&lt;/u&gt; pointing to a section inside the current page to have the semantic value of a link, otherwise it will be considered as &lt;code&gt;generic&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/URL"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Go to main page
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keyboard Interaction
&lt;/h3&gt;

&lt;p&gt;It can only be activated by pressing the key &lt;u&gt;Enter&lt;/u&gt;. If the key Space is pressed while the focus is on the link, the page will scroll down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screen Reader Interaction
&lt;/h3&gt;

&lt;p&gt;Screen Readers, generally, announce the links in the following way: &lt;strong&gt;Link, [accessible name of the link]&lt;/strong&gt;. It is extremely important to provide a descriptive and correct accessible name to the element.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bad Practice
&lt;/h3&gt;

&lt;p&gt;It is completely unnaceptable, a bad practice and goes against the native behavior of the element, forcing it to behave as a button by doing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"javascript:void(0)"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"openModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Open Menu
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"button()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Link with role button
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to do this, it means that you need a button.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a button?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Definition
&lt;/h3&gt;

&lt;p&gt;A &lt;u&gt;button&lt;/u&gt; is an interactive element that dispatches an action inside the page where it is located. It does not redirect the user to another place or location nor modifies the url. The actions that are being dispatched can be: open a modal, play a video, post a comment, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic
&lt;/h3&gt;

&lt;p&gt;The button needs the attribute &lt;u&gt;type&lt;/u&gt; with a value according to its action:&lt;br&gt;
    - &lt;strong&gt;type="button"&lt;/strong&gt;: it is used when the button does not have a default behavior.&lt;br&gt;
    - &lt;strong&gt;type="submit"&lt;/strong&gt;: it is used when the button sends information to a server.&lt;br&gt;
    - &lt;strong&gt;type="reset"&lt;/strong&gt;: it is used to reset all the values of an input or inputs to its initial value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;”button”&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"openModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Open Modal
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keyboard Interaction
&lt;/h3&gt;

&lt;p&gt;It can only be activated by pressing the keys &lt;u&gt;Enter&lt;/u&gt; or &lt;u&gt;Space&lt;/u&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screen Readers Interaction
&lt;/h3&gt;

&lt;p&gt;Screen Readers, generally, announce buttons in the following way: &lt;strong&gt;"Button, [Accessible Name of the button]"&lt;/strong&gt;. It is extremely important to provide a descriptive and correct accessible name to the element.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bad Practice
&lt;/h3&gt;

&lt;p&gt;It is completely unnaceptable, a bad practice and goes against the native behavior of the element, forcing it to behave as a link by doing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"window.location.href='/'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Go to homepage
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to do this, it means that you need a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should I use: link or button?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do you need an expandable section? use a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; with the state &lt;code&gt;aria-expanded&lt;/code&gt; and the focus should stay put.&lt;/li&gt;
&lt;li&gt;Do you need to jump to a section on the same page? use an &lt;code&gt;&amp;lt;a href="#section"&amp;gt;&lt;/code&gt;. There is no page load, updated the url, lands the user at a destination.&lt;/li&gt;
&lt;li&gt;Do you need to load more items into the current list? use a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; and when the items load, the focus should be placed in the first item of the new items.&lt;/li&gt;
&lt;li&gt;Do you need to redirect the user to a new location to read more about something? use an &lt;code&gt;&amp;lt;a href="/destination"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do you need to open a menu? use a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; with the states &lt;code&gt;aria-haspopup&lt;/code&gt; and &lt;code&gt;aria-expanded&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do you need to play or pause a video? use a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do you need to dowload a file? use a &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; with the attribute &lt;u&gt;download&lt;/u&gt;.&lt;/li&gt;
&lt;li&gt;Do you need a card that leads to a detail page? use a &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; stretched &lt;code&gt;::after&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Focus Style for Interactive Elements
&lt;/h2&gt;

&lt;p&gt;It is vital for keyboard and screen reader users to have a visual and logic focus on interactive elements. Never do &lt;code&gt;outline: none&lt;/code&gt; without providing a focus style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#0066ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>html</category>
    </item>
    <item>
      <title>Enlace o botón, esa es la cuestión</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:33:01 +0000</pubDate>
      <link>https://dev.to/micaavigliano/un-enlace-o-un-boton-55d4</link>
      <guid>https://dev.to/micaavigliano/un-enlace-o-un-boton-55d4</guid>
      <description>&lt;h2&gt;
  
  
  ¿Qué es un enlace?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Definición
&lt;/h3&gt;

&lt;p&gt;Un link/enlace/elemento ancla es un elemento interactivo que redirecciona al usuario a una nueva ubicación la cual puede ser una página diferente, una ubicación distinta en la misma página (se modifica el URL en ese caso con un parametro &lt;strong&gt;#&lt;/strong&gt;) o también se puede utilizar para descagar un archivo, entre otras cosas. Al activarse, lleva al usuario a la URL definida en su href. El navegador guarda esa navegación en su historial, de modo que el usuario puede volver a la página anterior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semántica
&lt;/h3&gt;

&lt;p&gt;Para que el elemento tenga carga semántica de elemento interactivo, tiene que si o si tener un atributo &lt;code&gt;href&lt;/code&gt; con una &lt;code&gt;URL&lt;/code&gt; válida o un &lt;code&gt;IDREF&lt;/code&gt; que apunte a un elemento en la misma página, de lo contrario va a ser tratado como un elemento &lt;code&gt;genérico&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/URL"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Ir a la página principal
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Interacción con el teclado
&lt;/h3&gt;

&lt;p&gt;Solo se puede activar con la tecla &lt;code&gt;Enter&lt;/code&gt;. Si se presiona la tecla Space mientras el foco esta en el enlace, la página va a scrollear hacia abajo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interacción con lectores de pantalla
&lt;/h3&gt;

&lt;p&gt;Los lectores de pantalla, generalmente, anuncian el elemento de la siguiente manera: &lt;strong&gt;"Enlace, [Nombre accessible del enlace]"&lt;/strong&gt; por lo cual es importante que el enlace tenga un nombre accesible pertinente y descriptivo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Malas prácticas
&lt;/h3&gt;

&lt;p&gt;Es totalmente inaceptable, una mala práctica y va en contra del funcionamiento nativo del elemento, forzar a un enlace a comportarse como un botón de la siguiente manera:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"javascript:void(0)"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"openModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Abrir menu
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"boton()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Enlace con rol botón
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si necesitas hacerlo, quiere decir que necesitas un botón&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué es un botón?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Definición
&lt;/h3&gt;

&lt;p&gt;Un botón es un elemento interactivo que al ser cliqueado ejecuta una acción dentro de la página donde se encuentra. &lt;u&gt;NO&lt;/u&gt; redirige al usuario a otro lugar, ni modifica la URL. Las acciones ejecutadas pueden ser abrir un modal, reproducir un vídeo, publicar un comentario, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semántica
&lt;/h3&gt;

&lt;p&gt;El botón necesita el atributo type según la acción que vaya a llevar a cabo.&lt;br&gt;
    - &lt;strong&gt;type="button"&lt;/strong&gt;: se utiliza si el botón no tiene un comportamiento determinado.&lt;br&gt;
    - &lt;strong&gt;type="submit"&lt;/strong&gt;: se utiliza si el botón envía información a un servidor.&lt;br&gt;
    - &lt;strong&gt;type="reset"&lt;/strong&gt;: se utiliza para resetear todos los inputs de un formulario a su valor inicial.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;”button”&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"openModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Abrir modal
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Interacción con el teclado
&lt;/h3&gt;

&lt;p&gt;Solo se puede activar con la teclas &lt;u&gt;Enter&lt;/u&gt; o &lt;u&gt;Space&lt;/u&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interacción con lectores de pantalla
&lt;/h3&gt;

&lt;p&gt;Los lectores de pantalla, generalmente, anuncian el elemento de la siguiente manera: &lt;strong&gt;"Botón, [nombre accessible del botón]"&lt;/strong&gt; por lo cual es importante que el botón tenga un nombre accesible pertinente y descriptivo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mala práctica
&lt;/h3&gt;

&lt;p&gt;Es totalmente inaceptable, una mala práctica y va en contra del funcionamiento nativo del elemento, forzar a un enlace a comportarse como un botón de la siguiente manera:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"window.location.href='/'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Ir a página de inicio
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si necesitas hacerlo, quiere decir que necesitas un enlace&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Enlace o botón? ¿Cuál debo usar según mi necesidad?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿Necesitas expandir una sección? Usá un &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; con el estado &lt;code&gt;aria-expanded&lt;/code&gt; y el foco debe mantenerse en el botón.&lt;/li&gt;
&lt;li&gt;¿Necesitas saltar a una sección dentro de la misma página? Usa un &lt;code&gt;&amp;lt;a href="#section"&amp;gt;&lt;/code&gt;. No tiene que haber una actualización de la página, se modifica el URL con la nueva referencia ID y se redirige al usuario a la nueva ubicación.&lt;/li&gt;
&lt;li&gt;¿Necesitas cargar más ítems en una lista? Usa un &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; y cuando los nuevos ítems se carguen, el foco se debe ubicar en el primer nuevo ítem.&lt;/li&gt;
&lt;li&gt;¿Necesitas redirigir al usuario a una nueva ubicación para que lea más sobre algo? Usa un &lt;code&gt;&amp;lt;a href="/destination"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;¿Necesitas abrir un menú? Usa un &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; con los estados &lt;code&gt;aria-haspopup&lt;/code&gt; y &lt;code&gt;aria-expanded&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;¿Necesitas reproducir o pausar un vídeo? Usa un &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;¿Necesitas descargar un archivo? Usa un &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; con el atributo &lt;u&gt;download&lt;/u&gt;.&lt;/li&gt;
&lt;li&gt;¿Necesitas que una sección lleve al usuario a la página de detalles? Usa un &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; estirado con un &lt;code&gt;::after&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Estilo del foco para elementos interactivos
&lt;/h2&gt;

&lt;p&gt;Para los usuarios de teclado y lectores de pantalla es vital que los elementos interactivos tengan una un foco visual y lógico. Nunca hagas &lt;code&gt;outline: none&lt;/code&gt; sin antes estilar el foco:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#0066ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>html</category>
      <category>a11y</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Accessible and Functional Quantity Spinbutton Pattern</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Wed, 13 May 2026 10:35:07 +0000</pubDate>
      <link>https://dev.to/micaavigliano/accessible-and-functional-quantity-spinbutton-pattern-3f40</link>
      <guid>https://dev.to/micaavigliano/accessible-and-functional-quantity-spinbutton-pattern-3f40</guid>
      <description>&lt;p&gt;Hey! This is Mica writing, a human not an AI agent (nothing against them, not discrimination of any kind is allowed here). Just a brief disclaimer before starting: this post was non AI-generated (I am not going to lie, I tried to use it for some research but since the errors I found were not properly documented the AI invented answers, lol) because I felt the urgent need to start using my brain and hands to craft something from scratch. If you are a human, let's connect, at the end you will find the ways to reach me. I am on a doom scrolling detox so you won't find me on instagram or twitter now. So, I have a question: &lt;a href="https://www.youtube.com/watch?v=Aiu5Fr7-hFQ&amp;amp;list=RDAiu5Fr7-hFQ&amp;amp;start_radio=1" rel="noopener noreferrer"&gt;Is there anybody out there?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you work on an e-commerce, a booking (hotels, flights, trains, etc) platform, a restaurant reservation system maybe you are familiar with the quantity spinbutton component. It is a common UI pattern that allows users to select a quantity of items they want to purchase or book. It is configured by two buttons and one input; one button for increasing the quantity, another button to decrease the quantity and an input where the user is able to type the desired amount or increase/decrease the quantity using the Up Arrow or Down Arrow accordingly. Sounds simple, and yes it is, but it is also a critical flow in the user journey. If it's badly implemented, you can not only be potentially losing sales (translation: losing money, which is important) but also creating a frustrating experience for users with disabilities (which should be more important than money, but those are my morals and ethics, lol). My goal in this article is setting a clear basis of a non-negotiable structure and behavior of the &lt;strong&gt;&lt;u&gt;quantity spinbutton pattern&lt;/u&gt;&lt;/strong&gt; and from there, explore different variations depending on the use case.&lt;/p&gt;

&lt;p&gt;Disclaimer: This is not a written-in-stone guide, I can make mistakes, so please let me know if you disagree with something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expected behaviors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Keyboard
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Up Arrow&lt;/td&gt;
&lt;td&gt;Increases the value by its step value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Down Arrow&lt;/td&gt;
&lt;td&gt;Decreases the value by its step value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;If the spinbutton has a minimum value, sets the value to its minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End&lt;/td&gt;
&lt;td&gt;If the spinbutton has a maximum value, sets the value to its maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Command or Fn + Right Arrow
&lt;/td&gt;
&lt;td&gt;Substitute for Apple's keyboards that do not have a End key to set the value to its maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
Command or Fn + Left Arrow
&lt;/td&gt;
&lt;td&gt;Substitute for Apple's keyboards that do not have a End key to set the value to its minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Screen Readers
&lt;/h3&gt;

&lt;h4&gt;
  
  
  NVDA Navigation
&lt;/h4&gt;

&lt;p&gt;You can download NVDA &lt;a href="https://www.nvaccess.org/download/" rel="noopener noreferrer"&gt;here&lt;/a&gt; and it's only available for the Windows operating system. It is compatible with Chrome, Firefox and Edge browsers.&lt;br&gt;
Long story short, an NVDA modifier key is needed to navigate using this screen reader. The modifier key can be Insert key (by default) or it can be remapped to the Caps Lock in the settings. Two modes for different purposes can be found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browse Mode&lt;/strong&gt;: Browse mode is used when reading documents or web pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus Mode&lt;/strong&gt;: Focus mode is used when the user enters a form or other fields that require user input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NVDA automatically switches between Browse and Focus modes, but the user can toggle them using Insert + Space Bar.&lt;br&gt;
| Command | Task |&lt;br&gt;
|--------|-----|&lt;br&gt;
| Down Arrow (Browse Mode) | Read next item |&lt;br&gt;
| Enter or Space Bar | Activate button |&lt;br&gt;
| Down Arrow (Focus Mode) | Decrement by step |&lt;br&gt;
| Up Arrow (Focus Mode) | Increment by step |&lt;br&gt;
| Home | Jump to aria-valuemin (via your custom handler) |&lt;br&gt;
| End | Jump to aria-valuemax (via your custom handler) |&lt;br&gt;
| Tab | Commit value, leave the field, return to browse mode |&lt;/p&gt;
&lt;h4&gt;
  
  
  VoiceOver Navigation
&lt;/h4&gt;

&lt;p&gt;If you are a macOS user, VoiceOver comes built into the operating system of your MacBook. It's compatible with the Safari browser.&lt;br&gt;
VoiceOver uses the Control and Option keys before each command. This combination is called &lt;strong&gt;VO&lt;/strong&gt; and these keys can be locked/unlocked by pressing Control + Option + ;(semicolon) all together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
Tab/Shift + Tab
&lt;/td&gt;
&lt;td&gt;Go to next/previous focusable item (link, button, input, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Space Bar
&lt;/td&gt;
&lt;td&gt;Activate a link or form control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Right Arrow
&lt;/td&gt;
&lt;td&gt;Move VO cursor to next element (not limited to focusable items)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Left Arrow
&lt;/td&gt;
&lt;td&gt;Move VO cursor to previous element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
VO + Shift + Down Arrow
&lt;/td&gt;
&lt;td&gt;Once the VO cursor is on the spinbutton, this combination enters the spinbutton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Up Arrow&lt;/td&gt;
&lt;td&gt;Once inside the spinbutton, increment by step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Down Arrow&lt;/td&gt;
&lt;td&gt;Once inside the spinbutton, decrement by step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;Jump to aria-valuemin (via your custom handler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End&lt;/td&gt;
&lt;td&gt;Jump to aria-valuemax (via your custom handler)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Gestures VoiceOver - iOS
&lt;/h4&gt;

&lt;p&gt;If you are an iOS user, VoiceOver is the screen reader by default on the operating system of your iPhone. It's compatible with the Safari browser. Unlike VoiceOver on macOS, VoiceOver on iOS, the navigation is controlled by finger gestures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gesture&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe next&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read next item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe left&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read previous item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Double-tap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Activate (link, button)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe up&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, increase the value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe down&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, decrease the value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Gestures Talkback - Android
&lt;/h4&gt;

&lt;p&gt;If you are an Android user, TalkBack is the default screen reader on the operating system of your mobile. It's compatible with Chrome, Firefox and Edge. Like VoiceOver for iOS, the navigation is controlled by gestures with the fingers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gesture&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe next&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read next item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe left&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read previous item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Double-tap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Activate (link, button)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe up&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, increase the value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swipe down&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the VO cursor is over the spinbutton, decrease the value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Pattern A: APG (ARIA Authoring Practices Guide) Spinbutton by W3C
&lt;/h2&gt;

&lt;p&gt;Currently everyone is following the &lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/" rel="noopener noreferrer"&gt;APG (ARIA Authoring Practices Guide)&lt;/a&gt; as if it were the bible and not as what it is: a &lt;strong&gt;&lt;u&gt;guide&lt;/u&gt;&lt;/strong&gt;, a very useful guide, btw, THE guide. Everything written there is considered a commandment written in stone by &lt;u&gt;non-accessibility experts&lt;/u&gt; (the people behind W3C are doing an amazing work and I only have gratitude and admiration towards them) and this might be a problem. The intention is good but the execution is not. When you first land on the &lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/examples/quantity-spinbutton/" rel="noopener noreferrer"&gt;APG Spin Button Pattern&lt;/a&gt;, the first thing you see is an alert with the following warning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code in this example is not intended for production environments. Before using it for any purpose, read this to understand why.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and then a list of reasons why you should be critical of the pattern provided. In this case, the most important one is:&lt;/p&gt;

&lt;blockquote&gt;Robust accessibility can be further optimized by choosing implementation patterns that maximize use of semantic HTML and heeding the warning that No ARIA is better than Bad ARIA.&lt;/blockquote&gt;

&lt;p&gt;So, what does this mean? Well, a couple of important things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Always be critical of everything (a rule for life, tho)&lt;/li&gt;
&lt;li&gt;Keep in mind the necessities and limitations of your product/business, and (more importantly) comply with the regulations and legislation in order to guarantee freedom of navigation through your product for users with disabilities.&lt;/li&gt;
&lt;li&gt;There is &lt;strong&gt;&lt;u&gt;always&lt;/u&gt;&lt;/strong&gt; (well, almost 80% always) an HTML equivalent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After this brief introduction to the APG, I would like to dive into its &lt;strong&gt;&lt;u&gt;Spinbutton pattern&lt;/u&gt;&lt;/strong&gt;. &lt;a href="https://www.w3.org/" rel="noopener noreferrer"&gt;W3C (The World Wide Web Consortium)&lt;/a&gt; presents us a solution using the WAI-ARIA &lt;code&gt;role="spinbutton"&lt;/code&gt; on the &lt;code&gt;input&lt;/code&gt; tag instead of using the native HTML solutions provided by the tag itself, the buttons are removed from the focus flow because they have a &lt;code&gt;tabindex="-1"&lt;/code&gt; but they are still operable via mouse and by using screen readers. Only the tag with the &lt;code&gt;role="spinbutton"&lt;/code&gt; is not fully semantic by itself and we will need to add the keyboard and screen reader functionalities programmatically and make it semantic by adding properties and states to the input element. &lt;/p&gt;

&lt;p&gt;In the accessibility field we have a mantra: &lt;strong&gt;No ARIA is better than Bad ARIA&lt;/strong&gt;. You should be extra careful when using ARIA because, if it is not used correctly, you will end up creating more issues than solving them. Again, the intention is good but the outcome is more barriers. Last month, &lt;a href="https://webaim.org/" rel="noopener noreferrer"&gt;WebAIM org&lt;/a&gt; launched its annual report for 2026 about the state of accessibility in over 1 million pages: &lt;a href="https://webaim.org/projects/million/" rel="noopener noreferrer"&gt;The WebAIM Million&lt;/a&gt;. The report shows that in 2026, accessibility errors increased by 10.1% according to the WCAG 2.2 Level A/AA conformance failures and concluded with the following reflection: &lt;/p&gt;

&lt;blockquote&gt;The 2026 WebAIM Million analysis found notable increases in both the number of detected accessibility errors and number of pages with WCAG conformance failures, reversing a trend of gradual accessibility improvements in recent years. A primary concern is the significant increase in home page complexity and ARIA code, both of which correlate to increased detectable errors.
These trends likely reflect broader shifts in web development including increased reliance on 3rd party frameworks and libraries and automated or AI-assisted coding practices (“vibe coding”). Home pages are getting larger and more technologically complex at an alarming rate, making accessibility more difficult to achieve and maintain. A key takeaway from this year's report is improving accessibility at scale will require both better practices and simpler systems. Alternatively, complex systems need to do a better job of focusing on accessibility fundamentals.&lt;/blockquote&gt;

&lt;p&gt;It's not a surprise that creating components using AI without questioning the output will end up creating more barriers for users with disabilities, and sloppier websites overall. Also, the lack of native and accessible examples on the web makes the AI coding agents return inaccessible components, but okay not everything is the fault of the AI, to be honest.&lt;/p&gt;
&lt;h3&gt;
  
  
  Functional Example – APG
&lt;/h3&gt;

&lt;p&gt;you can find the functional example &lt;a href="https://micaavigliano.com/en/blog/quantity-spinbutton-pattern#functional-example-apg" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  APG code
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"add-to-bag"&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"spinbutton"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
  &lt;span class="na"&gt;inputMode=&lt;/span&gt;&lt;span class="s"&gt;"numeric"&lt;/span&gt;
  &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"[0-9]*"&lt;/span&gt;
  &lt;span class="na"&gt;autoComplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;
  &lt;span class="na"&gt;spellCheck=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuemin=&lt;/span&gt;&lt;span class="s"&gt;{MIN}&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuemax=&lt;/span&gt;&lt;span class="s"&gt;{MAX}&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuenow=&lt;/span&gt;&lt;span class="s"&gt;{isValid&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="na"&gt;numeric&lt;/span&gt; &lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="na"&gt;undefined&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;aria-valuetext=&lt;/span&gt;&lt;span class="s"&gt;{value}&lt;/span&gt;
  &lt;span class="na"&gt;aria-invalid=&lt;/span&gt;&lt;span class="s"&gt;{!isValid&lt;/span&gt; &lt;span class="err"&gt;||&lt;/span&gt; &lt;span class="na"&gt;undefined&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;aria-describedby=&lt;/span&gt;&lt;span class="s"&gt;{isValid&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;help-add-to-bag&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;help-add-to-bag&lt;/span&gt; &lt;span class="na"&gt;error-add-to-bag&lt;/span&gt;&lt;span class="err"&gt;'}&lt;/span&gt;
  &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;{value}&lt;/span&gt;
  &lt;span class="na"&gt;onChange=&lt;/span&gt;&lt;span class="s"&gt;{handleChange}&lt;/span&gt;
  &lt;span class="na"&gt;onKeyDown=&lt;/span&gt;&lt;span class="s"&gt;{handleKeyDown}&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt;: to relate the &lt;code&gt;&amp;lt;input /&amp;gt;&lt;/code&gt; with the &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; element&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role="spinbutton"&lt;/code&gt;: this is a &lt;a href="https://www.w3.org/TR/wai-aria/#spinbutton" rel="noopener noreferrer"&gt;WAI-ARIA role&lt;/a&gt; that allows the user to increase and decrease a value within a given range. This role can be used on &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; or on a non-semantic element such as &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. The APG pattern for the &lt;code&gt;spinbutton&lt;/code&gt; component proposes the &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; with a &lt;code&gt;type="text"&lt;/code&gt; making the development more challenging.  The expected keyboard behavior for a &lt;code&gt;spinbutton&lt;/code&gt; is being able to increment or decrement the value by pressing the up/down arrow accordingly. Since this is not a native interaction of the &lt;code&gt;role="spinbutton"&lt;/code&gt; or of the &lt;code&gt;&amp;lt;input type="text" /&amp;gt;&lt;/code&gt;, we have to programmatically cover these behaviors. The &lt;code&gt;spinbutton&lt;/code&gt; element with type text generally comes with two buttons (one for decrement and the other to increment the value) that should be excluded from the navigation flow by adding &lt;code&gt;tabIndex={-1}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type="text"&lt;/code&gt;: implies that the &lt;code&gt;spinbutton&lt;/code&gt; will receive a value of type text, which is a pain because it means that we have to transform the type text to type number later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inputMode="numeric"&lt;/code&gt;: since the input is type text, we have to make the browsers know which virtual keyboard they will have to display. This is vital for mobile navigation because without the value &lt;u&gt;numeric&lt;/u&gt; the virtual keyboard display to enter a value will be a QWERTY instead of a numeric input keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pattern="[0-9]*"&lt;/code&gt;: again, since the input is &lt;code&gt;type="text"&lt;/code&gt;, we have to use a regex to only admit values of type number. (This attribute was not on the APG example, but I cannot help myself).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autocomplete="off"&lt;/code&gt;: it is used on inputs that take a text or numeric value as input to prevent the browser from automatically entering a value into this field. (again, my take, not on APG example).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-valuemin="[NUMBER]"&lt;/code&gt;: sets the minimum value allowed for the spinbutton. The default is 0, if no value is passed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-valuemax="[NUMBER]"&lt;/code&gt;: sets the maximum value allowed for the spinbutton. The default is 100, if no value is passed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-valuenow="[number]"&lt;/code&gt;: here things start to get interesting. By definition, this attribute defines the current value for a range widget (meter, scrollbar, slider and spinbutton). Since we are not using a semantic solution, this value should be updated programmatically. Also, this value is intended for numeric values and APG provides an example where the &lt;code&gt;spinbutton&lt;/code&gt; is being considered as a text, so we have to also transform the value from type string to type number. This is not everything! Since the &lt;code&gt;spinbutton&lt;/code&gt; is not correctly typed, the announcement of the current value will be done as a percent by VoiceOver (macOS and iOS) announces it as a percent, TalkBack on Pixel 10 does not announce it at all, and NVDA announces it correctly. You can test it on the &lt;a href="https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/examples/quantity-spinbutton/#ex_label" rel="noopener noreferrer"&gt;APG example&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-invalid="[boolean]"&lt;/code&gt;: indicates that the current value is invalid.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby="[IDREF]"&lt;/code&gt;: this attribute is used to establish a relationship between widgets or groups and the text that describes them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Conclusion for the APG Spinbutton Pattern
&lt;/h3&gt;

&lt;p&gt;To articulate a conclusion for this pattern, I drew on &lt;a href="https://www.w3.org/TR/core-aam-1.2/" rel="noopener noreferrer"&gt;Core Accessibility API Mappings 1.2&lt;/a&gt; which is a guide that specifies how WAI-ARIA roles, states, and properties are expected to be exposed by user agents (browsers, in this case) via platform accessibility APIs. &lt;br&gt;&lt;br&gt;
Since a &lt;code&gt;spinbutton&lt;/code&gt; is considered a range widget, it's implicitly expected to receive a value of type number even though the APG pattern tells us otherwise. By implementing a role &lt;code&gt;spinbutton&lt;/code&gt; in a text input, we will be going against the nature of the role, which leads us to do extra work to make it work correctly.&lt;br&gt;
The pattern is good but we can make it better, with some browser limitations (the party can never be enjoyed in peace), with the native HTML solution as we will see below.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern B: Native HTML solution
&lt;/h2&gt;

&lt;p&gt;Unfortunately, nothing is perfect; the native HTML solution for &lt;code&gt;spinbutton&lt;/code&gt; is tempting because it works well in most cases BUT has a few limitations that aren't HTML-related but browser-related, buuuh. &lt;br&gt;&lt;br&gt;
Browsers have different engines to create what we colloquially know as &lt;u&gt;Accessibility Tree&lt;/u&gt;. This tree is just an object with &lt;u&gt;nodes&lt;/u&gt;. Every node includes information about the HTML elements and attributes. Finally, this object is being exposed to the Accessibility APIs of every operating system so they can translate the information and make it readable to assistive technologies. Here is where the things get complicated because every browser engine sometimes interprets things differently. Below, you can find a list of the browser engines for the different browsers and the Accessibility APIs they support.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blink&lt;/strong&gt;: this is the browser engine for &lt;u&gt;Chrome&lt;/u&gt;, &lt;u&gt;Edge&lt;/u&gt;, &lt;u&gt;Opera&lt;/u&gt; and &lt;u&gt;Brave&lt;/u&gt; (all browsers based on Chromium) which means all the browsers are going to have the same Accessibility Tree as the one we can see on the following image:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4cap6mnpryi7wd1wwxb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4cap6mnpryi7wd1wwxb.png" alt="Chrome DevTools accessibility node showing the computed properties for a spinbutton: role spinbutton, name " width="716" height="94"&gt;&lt;/a&gt;&lt;br&gt;
Supported on the following Accessibility APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: IAccessible, IAccessible2 and UIAutomation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mac&lt;/strong&gt;: NSAccessibility (funny thing here is that Blink creates a more accurate and complete node than WebKit and VoiceOver announces the spinbutton better on Chrome, as we are going to read later)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: ATK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt;: AccessibilityNodeInfo and AccessibilityNodeProvider. I opened a ticket for this operating system because it is not exposing the &lt;code&gt;spinbutton&lt;/code&gt; correctly preventing the user from having an accessible experience. The ticket to Android can be seen &lt;a href="https://issues.chromium.org/issues/511602085" rel="noopener noreferrer"&gt;here&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gecko&lt;/strong&gt;: this is the browser engine for &lt;u&gt;Firefox&lt;/u&gt;. The Accessibility Tree it creates, in my opinion, is the most complete and it is compatible with the following Accessibility APIs:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: IAccessible, IAccessible2 and UIAutomation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mac&lt;/strong&gt;: mozAccessible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: ATK

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebKit&lt;/strong&gt;: this is the browser engine for &lt;u&gt;Safari&lt;/u&gt; in macOS and iOS. Here is where our problems start. The engine does not recognize the element as a &lt;code&gt;spinbutton&lt;/code&gt; but as a &lt;code&gt;role="textbox"&lt;/code&gt; if we don't explicitly pass the &lt;code&gt;role="spinbutton"&lt;/code&gt; to the &lt;code&gt;&amp;lt;input type="text" /&amp;gt;&lt;/code&gt;. This is an issue because the VoiceOver screen reader in both devices, desktop and mobile, will prevent the user from having an accessible experience, the same issue that I faced on Android.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1q2kez40al3kgwddta6t.png" alt="'Safari Web Inspector accessibility panel showing: Role textbox (default), Label " width="436" height="326"&gt;
Again, I ended up opening a ticket to Apple about this behavior. The ticket to Apple can be seen &lt;a href="https://developer.apple.com/forums/thread/825940" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Functional Example – HTML
&lt;/h3&gt;

&lt;p&gt;The native functional example can be found &lt;a href="https://micaavigliano.com/en/blog/quantity-spinbutton-pattern#functional-example-html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  HTML native code
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"add-to-bag-native"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"spinbutton"&lt;/span&gt;
  &lt;span class="na"&gt;autoComplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;
  &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;{MIN}&lt;/span&gt;
  &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;{MAX}&lt;/span&gt;
  &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;{STEP}&lt;/span&gt;
  &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;{value}&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt;: same thing as in the APG pattern&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type="number"&lt;/code&gt;: this is what natively converts an input into a &lt;code&gt;spinbutton&lt;/code&gt; and restricts it to numeric values. It natively supports supports increasing and decreasing the value by: pressing the native controls that come with the input &lt;code&gt;type="number"&lt;/code&gt; (see/read/perceive the image below), pressing Up Arrow or Down Arrow, or swiping (on mobile with the screen readers on).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdrioi2uty3leg9dxuod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdrioi2uty3leg9dxuod.png" alt="A spinbutton with a single value with controls to increase or decrease a value. Default appearance and accessibility behavior vary substantially across browser and platform combinations" width="148" height="138"&gt;&lt;/a&gt;&lt;br&gt;
  The appearance of these arrows inside the input comes by default and we can override it if we provide another mechanism to substitute the buttons themselves; we already did that with the custom buttons to increase and decrease the value, so we are good to go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;  &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"number"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;appearance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;textfield&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"number"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-outer-spin-button&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"number"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;::-webkit-inner-spin-button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;-webkit-appearance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;appearance: textfield;&lt;/code&gt;, we need it to strip the native spinbutton styling in Firefox&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;::-webkit-outer-spin-button&lt;/code&gt; and &lt;code&gt;::-webkit-inner-spin-button&lt;/code&gt; are pseudo-selectors that only Blink and WebKit recognize and the &lt;code&gt;-webkit-appearance: none&lt;/code&gt; property strips the native spinbutton styling in Chrome and Safari.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;role="spinbutton"&lt;/code&gt;: well, having in mind all the compatibility issues we have in different browsers, I found out that if you &lt;strong&gt;only&lt;/strong&gt; pass the WAI-ARIA &lt;code&gt;role="spinbutton"&lt;/code&gt; magically they all disappear. So, there is no harm in adding it to prevent headaches in the future. This post was built for this moment, you can thank me.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autoComplete="off"&lt;/code&gt;: just the same as in the APG example&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;min={NUMBER}&lt;/code&gt;: the minimum value to accept for this input and should be less than or equal to the maximum value. If a value that isn't a valid number is specified, then the input has no minimum.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max={NUMBER}&lt;/code&gt;: the maximum value to accept for this input and should be greater than or equal to the minimum value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;step={NUMBER}&lt;/code&gt;: defines the allowed interval between valid values. The default is 1 and only accepts integers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;value={NUMBER}&lt;/code&gt;: a number representing the value of the number entered into the input&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion for the HTML native solution
&lt;/h3&gt;

&lt;p&gt;If you want to have a functional, accessible and widely supported across browsers &lt;code&gt;spinbutton&lt;/code&gt; component, I recommend using my solution and to always go to the native solutions and, if you do not feel comfortable enough, talk to your trustworthy accessibility expert. This native example with the workaround of adding a WAI-ARIA &lt;code&gt;role="spinbutton"&lt;/code&gt; to force its semantic value on some browser engines was tested on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome (version 148) + macOS (Tahoe version 26.4.1) + VoiceOver&lt;/li&gt;
&lt;li&gt;Chrome (version 148) + iOS (version 26.4.1)/ iPhone pro 15 + VoiceOver&lt;/li&gt;
&lt;li&gt;Chrome (version 148) + Windows 11 + NVDA&lt;/li&gt;
&lt;li&gt;Chrome (version 148) + Android/Pixel 10 (version 16) + TalkBack&lt;/li&gt;
&lt;li&gt;Firefox (version 150) + Windows 11 + NVDA&lt;/li&gt;
&lt;li&gt;Firefox (version 150) + macOS (Tahoe version 26.4.1)&lt;/li&gt;
&lt;li&gt;Safari (version 26.4) + macOS (Tahoe version 26.4.1) + VoiceOver&lt;/li&gt;
&lt;li&gt;Safari (version 26.4) + iOS (version 26.4.1) + VoiceOver&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I did not cover but it's important to mention: the Home and End keyboard shortcuts mentioned on the Keyboard section are not provided natively so you have to cover them programmatically. Also, some keyboards, such as Apple's keyboards (always Apple) do not have keys for Home and End and they are substituted by the combination Command or or Fn + Right/Left Arrows so you have to cover this combination as well. It's just a function of a 9-line function, nothing too exceptional, you should contemplate it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleKeyDown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;End&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metaKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ArrowRight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metaKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ArrowLeft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Having a semantic, functional and accessible &lt;code&gt;spinbutton&lt;/code&gt; on your e-commerce will not only contribute to improving the overall internet experience to everyone but it will leverage your product and increase your sales. A win-win for all parties involved; you get the money, the user, no matter how he/she/they are navigating, gets the product.&lt;br&gt;
According to &lt;a href="https://www.who.int/news-room/fact-sheets/detail/assistive-technology" rel="noopener noreferrer"&gt;WHO&lt;/a&gt; more than 2.5 billion people need one or more assistive products to to meet their daily needs, and one of those needs is the right to navigate freely through the web. You do not really know who is navigating through your website behind the screen, could be a human with different needs or now an AI AGENT (!!!!). Omg, a new party entered the game and should be contemplated too. Nowadays, some users are relying on autonomous purchase bots, voice shopping assistants or AI browsing agents. These AI assistants base their navigation on the web in the same way as any other assistive technology: they consume the data provided by the Accessibility Tree. Amazing. &lt;br&gt;&lt;br&gt;
Well, we learned that: a new player entered the game (AI assistants), the native HTML solution is always the best option (might be buggy but it's the browser's fault) and you should always be critical and test everything you are building.&lt;br&gt;&lt;/p&gt;

&lt;p&gt;You can find me on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email: &lt;a href="//micaela.avigliano@gmail.com"&gt;micaela.avigliano@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;linkedin: &lt;a href="https://www.linkedin.com/in/micaelaavigliano/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/micaelaavigliano/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Accessible Pretext demo</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Tue, 31 Mar 2026 13:07:24 +0000</pubDate>
      <link>https://dev.to/micaavigliano/accessible-pretext-demo-1492</link>
      <guid>https://dev.to/micaavigliano/accessible-pretext-demo-1492</guid>
      <description>&lt;h2&gt;
  
  
  What I did
&lt;/h2&gt;

&lt;p&gt;I took the &lt;a href="https://somnai-dreams.github.io/pretext-demos/the-editorial-engine.html" rel="noopener noreferrer"&gt;demo for a editorial engine&lt;/a&gt; built with &lt;a href="https://github.com/chenglou/pretext" rel="noopener noreferrer"&gt;pretext&lt;/a&gt; by Cheng Lou and I made it fully accessible, with semantic HTML structure, keyboard operability, screen reader support, and &lt;code&gt;prefers-reduced-motion&lt;/code&gt; compliance. The result is a high-performance text layout demo that meets WCAG 2.2 success criteria while not compromising aesthetics and performance. The orbs are still draggable with the mouse but I also added the possibility of moving them using the keyboard.&lt;/p&gt;

&lt;p&gt;The app uses a &lt;strong&gt;dual rendering system&lt;/strong&gt;. At normal zoom, pretext powers a visual stage where text lines are individually positioned and wrap dynamically around the orbs as circular obstacles. Underneath, a native HTML &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; with CSS &lt;code&gt;column-count&lt;/code&gt; remains in the DOM for screen readers, copy/paste, and find-in-page. At high zoom (≥150%) or narrow viewports (&lt;code&gt;&amp;lt;500px&lt;/code&gt;), the pretext stage is removed entirely and the native HTML article becomes the visible layout, ensuring text is always fully readable, selectable, and copyable. A pretext-rendered drop cap provides the decorative initial letter in the visual stage, while CSS &lt;code&gt;::first-letter&lt;/code&gt; handles it in the native fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pretext library usage
&lt;/h2&gt;

&lt;p&gt;Pretext powers the orb animation system. The following APIs are available in the project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Where used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prepareWithSegments()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Measures and caches word widths for text blocks&lt;/td&gt;
&lt;td&gt;Body text preparation on font load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;layoutNextLine()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lays out one line of text at a given max width&lt;/td&gt;
&lt;td&gt;Core layout loop — wraps text around orb obstacles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;layoutWithLines()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lays out all lines of a prepared text block&lt;/td&gt;
&lt;td&gt;Headline fitting to available width/height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;walkLineRanges()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Iterates line ranges without allocating line objects&lt;/td&gt;
&lt;td&gt;Text measurement utilities&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Accessibility improvements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Semantic HTML structure
&lt;/h3&gt;

&lt;p&gt;The app uses proper HTML5 landmarks and semantic elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt;: Fixed bar with controls and interaction hints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;: Primary content area&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;article lang="es"&amp;gt;&lt;/code&gt;: Full readable text as native HTML; &lt;code&gt;lang="es"&lt;/code&gt; switches screen readers to Spanish pronunciation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt;: Orb container with descriptive &lt;code&gt;aria-label&lt;/code&gt;, exposed as a named region&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt;: Performance stats and credits&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;kbd&amp;gt;&lt;/code&gt;: Keyboard shortcuts styled as keycaps. &lt;code&gt;aria-hidden&lt;/code&gt; prevents SR double-announcement; &lt;code&gt;sr-only&lt;/code&gt; siblings provide the readable text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;cite&amp;gt;&lt;/code&gt;: Book title attribution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;: Keyboard shortcuts and credits navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keyboard accessibility
&lt;/h2&gt;

&lt;p&gt;Every interactive element is operable without a mouse:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;With VoiceOver&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Toggle global pause&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigate to orbs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Tab&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move focused orb&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Arrow keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Option + Arrow keys&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause/resume individual orb&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Space&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Orbs are native &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; elements. &lt;code&gt;aria-roledescription="draggable orb"&lt;/code&gt; augments the role announcement so the user understands these are not ordinary buttons. Each orb's &lt;code&gt;aria-label&lt;/code&gt; is dynamic and encodes its position in the set, the available interactions, and its current pause state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screen reader orb interaction discovery
&lt;/h2&gt;

&lt;p&gt;When a VoiceOver user tabs to an orb, they hear: "Golden orb, 1 of 5. Use Option plus arrow keys to move. Press Space to pause." The instruction lives inside the &lt;code&gt;aria-label&lt;/code&gt; because VoiceOver captures plain arrow keys for its own navigation; the page tells the user which key combination it &lt;em&gt;can&lt;/em&gt; receive (&lt;code&gt;Option + Arrow&lt;/code&gt;) instead of attempting to override the screen reader. When the orb is paused, the label changes to "Press Space to resume."&lt;/p&gt;

&lt;h2&gt;
  
  
  Screen reader support
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aria-live="polite"&lt;/code&gt;&lt;/strong&gt;: Announces orb selection, pause/resume, and state changes without interrupting the user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aria-pressed&lt;/code&gt;&lt;/strong&gt;: Toggle buttons communicate their on/off state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aria-label&lt;/code&gt;&lt;/strong&gt;: Every interactive element has a descriptive, dynamic label&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;prefers-reduced-motion&lt;/code&gt; support
&lt;/h2&gt;

&lt;p&gt;When the user's OS or browser has reduced motion enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All orb animation stops, orbs render at their initial positions, static&lt;/li&gt;
&lt;li&gt;CSS &lt;code&gt;scroll-snap-type&lt;/code&gt; and &lt;code&gt;scroll-behavior: smooth&lt;/code&gt; are disabled&lt;/li&gt;
&lt;li&gt;CSS transitions and animations are suppressed globally via &lt;code&gt;animation-duration: 0.01ms !important&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A toggle button allows users to override this preference in-app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pause control
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global pause&lt;/strong&gt; (button or &lt;code&gt;Esc&lt;/code&gt;): Stops all orbs. Paused orbs freeze in their document position and don't move when the user scrolls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Individual pause&lt;/strong&gt; (click or &lt;code&gt;Space&lt;/code&gt; on a focused orb): Only that orb stops. Other orbs keep moving. This allows users to control exactly which elements are in motion.&lt;/li&gt;
&lt;li&gt;The global pause button reflects the aggregate state. If all orbs are individually paused, it shows "Play"; if all are moving, it shows "Pause".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Text content and layout
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dual rendering: pretext stage + native HTML
&lt;/h3&gt;

&lt;p&gt;Implementation details behind the dual layout described above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pretext visual stage&lt;/strong&gt; (&lt;code&gt;aria-hidden="true"&lt;/code&gt;): Each line is an absolutely positioned &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; laid out by &lt;code&gt;layoutNextLine()&lt;/code&gt;, wrapping around orbs as circular obstacles. The stage's height is computed from the lowest line position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native HTML article&lt;/strong&gt;: When the pretext stage is active, the &lt;code&gt;&amp;lt;article lang="es"&amp;gt;&lt;/code&gt; is visually hidden with &lt;code&gt;position: absolute; left: -9999px&lt;/code&gt; (not &lt;code&gt;display: none&lt;/code&gt;, so AT still reads it).&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;useNativeLayout&lt;/code&gt; state derived from &lt;code&gt;window.outerWidth / window.innerWidth&lt;/code&gt; decides which mode is active.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Viewport&lt;/th&gt;
&lt;th&gt;Columns (both modes)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt; 1000px&lt;/td&gt;
&lt;td&gt;3 columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;641-1000px&lt;/td&gt;
&lt;td&gt;2 columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≤ 640px&lt;/td&gt;
&lt;td&gt;1 column&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Text wrapping around orbs
&lt;/h3&gt;

&lt;p&gt;In the pretext stage, each orb's position and radius are converted to a circular obstacle via &lt;code&gt;orbToObstacle()&lt;/code&gt;. The layout engine (&lt;code&gt;layoutAllText()&lt;/code&gt;) feeds these obstacles to &lt;code&gt;layoutNextLine()&lt;/code&gt;, which shortens or shifts lines to flow around the orbs in real time as they move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Headline fitting
&lt;/h3&gt;

&lt;p&gt;The headline is dynamically sized using &lt;code&gt;fitHeadline()&lt;/code&gt;, which uses pretext's &lt;code&gt;layoutWithLines()&lt;/code&gt; to find the largest font size that fits the headline within the available width and a max height (35% of viewport, or 20% on short screens). Each headline line is rendered as a separate absolutely positioned element.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drop cap
&lt;/h3&gt;

&lt;p&gt;In the pretext stage, the first character of the first paragraph is rendered as a positioned &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; spanning 3 body lines in height. The layout engine reserves a rectangular region for the drop cap and flows the first paragraph's text around it. In native mode, CSS &lt;code&gt;::first-letter&lt;/code&gt; provides the same effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic header clearance
&lt;/h3&gt;

&lt;p&gt;The article's &lt;code&gt;padding-top&lt;/code&gt; uses a CSS custom property &lt;code&gt;--header-h&lt;/code&gt; set by a &lt;code&gt;ResizeObserver&lt;/code&gt; on the fixed header. In the pretext stage, the top gutter is computed as &lt;code&gt;Math.max(GUTTER, headerHeight + 8)&lt;/code&gt;. This ensures content is never hidden behind the header at any zoom level or viewport size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile and responsive behavior
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Orbs and pretext stage hidden on small screens and high zoom
&lt;/h3&gt;

&lt;p&gt;At the 500px / 150%-zoom threshold, React conditionally unmounts the orb &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; and the pretext stage entirely (not &lt;code&gt;display: none&lt;/code&gt;), and &lt;code&gt;useNativeLayout&lt;/code&gt; also halts the &lt;code&gt;renderFrame&lt;/code&gt; loop so no layout work runs in the background.&lt;/p&gt;

&lt;p&gt;Zoom detection uses &lt;code&gt;window.outerWidth / window.innerWidth&lt;/code&gt;: when the user zooms in, &lt;code&gt;innerWidth&lt;/code&gt; shrinks while &lt;code&gt;outerWidth&lt;/code&gt; stays constant, giving the actual zoom ratio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collapsible header at high zoom
&lt;/h3&gt;

&lt;p&gt;At 200%+ zoom or viewports below 500px, the header switches to a compact mode where all controls and keyboard shortcuts collapse behind a native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; toggle, preventing the fixed header from consuming the viewport at high magnification.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;ResizeObserver&lt;/code&gt; on the header keeps the &lt;code&gt;--header-h&lt;/code&gt; CSS custom property in sync as it expands or collapses, so article content always clears it without overlap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile header and footer
&lt;/h3&gt;

&lt;p&gt;On screens below 640px:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The header collapses to a single toggle (same &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; mechanism as high zoom)&lt;/li&gt;
&lt;li&gt;The footer stacks vertically (stats centered, credits below)&lt;/li&gt;
&lt;li&gt;Button text and hint font sizes reduce for touch targets&lt;/li&gt;
&lt;li&gt;Text reflows to a single readable column&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Scroll snapping
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;html { scroll-snap-type: y proximity; scroll-behavior: smooth; }&lt;/code&gt; snaps near-alignments without trapping keyboard scroll (&lt;code&gt;Space&lt;/code&gt;, &lt;code&gt;Page Down&lt;/code&gt;, arrows keep working because the type is &lt;code&gt;proximity&lt;/code&gt;, not &lt;code&gt;mandatory&lt;/code&gt;). Under &lt;code&gt;prefers-reduced-motion: reduce&lt;/code&gt;, both properties fall back to &lt;code&gt;auto&lt;/code&gt; / &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  WCAG success criteria
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.3.1 Info and Relationships&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Semantic HTML conveys structure programmatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.3.2 Meaningful Sequence&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;DOM order matches visual reading order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.3.4 Orientation&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Layout adapts to portrait and landscape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4.1 Use of Color&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pause state uses opacity + SR announcement, not color alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4.3 Contrast (Minimum)&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;All text meets 4.5:1 ratio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4.4 Resize Text&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Native HTML reflows at 200% zoom; orbs removed at 150%+ to prevent obstruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4.10 Reflow&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;CSS columns reflow to single column at 320px width, no horizontal scroll&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4.11 Non-text Contrast&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Focus indicators meet 3:1 against adjacent colors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.1.1 Keyboard&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;All functionality available via keyboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.1.2 No Keyboard Trap&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scroll-snap-type: proximity&lt;/code&gt; (not &lt;code&gt;mandatory&lt;/code&gt;), no focus traps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.2.2 Pause, Stop, Hide&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Global and per-orb pause controls, &lt;code&gt;Esc&lt;/code&gt; shortcut&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.3.3 Animation from Interactions&lt;/td&gt;
&lt;td&gt;AAA&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt; respected, manual toggle available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.4.1 Bypass Blocks&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Landmark navigation via &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.4.3 Focus Order&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Tab order follows logical document structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.1.1 Language of Page&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lang="en"&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, &lt;code&gt;lang="es"&lt;/code&gt; on Spanish text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.1.2 Language of Parts&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Spanish text scoped with &lt;code&gt;lang="es"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.1.2 Name, Role, Value&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Buttons have labels, toggles have &lt;code&gt;aria-pressed&lt;/code&gt;, live regions announce changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://claude.ai/" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; calculations, editing and README writing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt; 19&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.typescriptlang.org/" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt; 5.9&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vite.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt; 8&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/chenglou/pretext" rel="noopener noreferrer"&gt;@chenglou/pretext&lt;/a&gt; 0.0.3&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://brailleinstitute.org/freefont" rel="noopener noreferrer"&gt;Atkinson Hyperlegible&lt;/a&gt;, font designed by the Braille Institute for maximum readability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/micaelaavigliano/" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/micaavigliano" rel="noopener noreferrer"&gt;Github&lt;/a&gt;**&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Accessibility: 2026 predictions</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Sat, 03 Jan 2026 10:44:51 +0000</pubDate>
      <link>https://dev.to/micaavigliano/accessibility-2026-predictions-3g9</link>
      <guid>https://dev.to/micaavigliano/accessibility-2026-predictions-3g9</guid>
      <description>&lt;p&gt;I have been in the field for almost 6 years and talking (or writing) about accessibility had always been tough in the tech area but NOW it is insufferable. &lt;/p&gt;

&lt;p&gt;Nowadays, accessibility it is a hot topic, everyone want to get profit from it and be compliant (which is okay, taking in consideration the European Accessibility Act entering the game). Internet is full of bullshit post generated with AI, talking about the same shit over and over again that nobody reads because they are just empty words with no actual substance. &lt;/p&gt;

&lt;p&gt;This is not a anti-AI post. I consider it a valuable tool to boost our job. But, what happen when we forget our human condition in order to gain money and forget that we are building product not only for AI search robots but for HUMANS? &lt;/p&gt;

&lt;p&gt;Stop using overlays for accessibility.&lt;br&gt;
Stop using AI for writing posts, use it just for corrections. &lt;br&gt;
Use your hands and your brain.&lt;br&gt;
Think before writing.&lt;br&gt;
Start talking with actual accessibility web professionals.&lt;br&gt;
Talk with actual users with disabilities.&lt;/p&gt;

&lt;p&gt;Not everything is money and speed. Behind the computer there are people. Do not forget that.&lt;/p&gt;

&lt;p&gt;pd: the title is just a clickbait for my post-rage lol&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>react</category>
      <category>ai</category>
    </item>
    <item>
      <title>Accessible components: Pagination</title>
      <dc:creator>Mica</dc:creator>
      <pubDate>Tue, 29 Oct 2024 16:58:05 +0000</pubDate>
      <link>https://dev.to/micaavigliano/accessible-components-pagination-58c2</link>
      <guid>https://dev.to/micaavigliano/accessible-components-pagination-58c2</guid>
      <description>&lt;p&gt;Today, we're going to look at how to create pagination from scratch and make it accessible and reusable. I hope it’s helpful, and please leave your comments at the end of the post!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Github&lt;/strong&gt;: &lt;a href="https://github.com/micaavigliano/accessible-pagination" rel="noopener noreferrer"&gt;https://github.com/micaavigliano/accessible-pagination&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Proyecto&lt;/strong&gt;: &lt;a href="https://accessible-pagination.netlify.app/" rel="noopener noreferrer"&gt;https://accessible-pagination.netlify.app/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Custom hook to fetch data
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;network response failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;We'll create a custom hook with a &lt;strong&gt;generic type&lt;/strong&gt;. This will allow us to specify the expected data type when using this hook.&lt;/li&gt;
&lt;li&gt;We'll expect three parameters: one for the URL from which we'll fetch the data, &lt;strong&gt;currentPage&lt;/strong&gt;, which is the page we're on (default is 0), and &lt;strong&gt;pageSize&lt;/strong&gt;, which is the number of items per page (default is 20, but you can change this value).&lt;/li&gt;
&lt;li&gt;In our state const &lt;code&gt;[data, setData] = useState&amp;lt;T | null&amp;gt;(null);&lt;/code&gt;, we use the generic type &lt;code&gt;T&lt;/code&gt; since, as we use it for different data requests, we'll be expecting different data types.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Pagination
&lt;/h2&gt;

&lt;p&gt;To make pagination accessible, we need to consider the following points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focus should move through all interactive elements of the pagination and have a visible indicator.&lt;/li&gt;
&lt;li&gt;To ensure good interaction with screen readers, we must correctly use regions, properties, and states.&lt;/li&gt;
&lt;li&gt;Pagination should be grouped within a &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; tag and contain an &lt;code&gt;aria-label&lt;/code&gt; identifying it specifically as pagination.&lt;/li&gt;
&lt;li&gt;Each item within the pagination should have an &lt;code&gt;aria-setsize&lt;/code&gt; and an &lt;code&gt;aria-posinset&lt;/code&gt;. Now, what are these for? Well, &lt;code&gt;aria-setsize&lt;/code&gt; is used to calculate the total number of items within the pagination list. The screen reader will announce it as follows:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwvy2lefaozhiokxjaib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwvy2lefaozhiokxjaib.png" alt="Screenshot of a voiceover annoucement: list of 1859 items" width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aria-posinset&lt;/code&gt; is used to calculate the position of the item within the total number of items in the pagination. The screen reader will announce it as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F49cb5zmd8u5vz263clh0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F49cb5zmd8u5vz263clh0.png" alt="Screenshot of the screen reader voiceover that announces: go to page 1. Current page, button, position 1 of 1859" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each item should have an aria-label to indicate which page we’ll go to if we click on that button.&lt;/li&gt;
&lt;li&gt;Include buttons to go to the next/previous item, and each of these buttons should have its corresponding aria-label.&lt;/li&gt;
&lt;li&gt;If our pagination contains an ellipsis, it should be correctly marked with an aria-label.&lt;/li&gt;
&lt;li&gt;Every time we go to a new page, the screen reader should announce which page we are on and how many new items there are, as follows:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1cob9vqjsqm3ftryewy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1cob9vqjsqm3ftryewy.png" alt="Screenshot of the screen reader voiceover that announces: page 3 loaded. Showing 20 items" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To achieve this, we’re going to code it as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatusMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatusMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Page &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; loaded. Displaying &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;near_earth_objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the page finishes loading, we’ll set a new message with our &lt;code&gt;currentPage&lt;/code&gt; and the length of the new array we’re loading.&lt;/p&gt;

&lt;p&gt;Alright! Now let’s take a look at how the code is structured in the &lt;strong&gt;pagination.tsx&lt;/strong&gt; file.&lt;/p&gt;

&lt;p&gt;The component will require five props.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaginationProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;nextPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;prevPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;goToPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;currentPage&lt;/code&gt; refers to the current page. We’ll manage it within the component where we want to use pagination as follows: &lt;code&gt;const [currentPage, setCurrentPage] = useState&amp;lt;number&amp;gt;(1)&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;totalPages&lt;/code&gt; refers to the total number of items to display that the API contains.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nextPage&lt;/code&gt; is a function that will allow us to go to the next page and update our currentPage state as follows:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePageChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setCurrentPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handlePageChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prevPage&lt;/code&gt; is a function that will allow us to go to the page before our current page and update our &lt;code&gt;currentPage&lt;/code&gt; state.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prevPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handlePageChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;goToPage&lt;/code&gt; is a function that will need a numeric parameter and is the function each item will use to navigate to the desired page. We’ll make it work as follows:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePageChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setCurrentPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To bring our pagination to life, we need one more step: creating the array that we’ll iterate through in our list! For that, we should follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a function; I’ll call it &lt;code&gt;getPageNumbers&lt;/code&gt; in this case.&lt;/li&gt;
&lt;li&gt;Create variables for the first and last items in the list.&lt;/li&gt;
&lt;li&gt;Create a variable for the left-side ellipsis. I’ve decided to place my ellipsis after the fourth item in the list.&lt;/li&gt;
&lt;li&gt;Create a variable for the right-side ellipsis. I’ve decided to place my ellipsis just before the last three items in the list.&lt;/li&gt;
&lt;li&gt;Create a function that returns an array with 5 centered items: the current page, two previous items, and two subsequent items. If needed, we’ll exclude the first and last pages.
&lt;code&gt;const pagesAroundCurrent = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2].filter(page =&amp;gt; page &amp;gt; firstPage &amp;amp;&amp;amp; page &amp;lt; lastPage);&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For our final variable, we’ll create an array that contains all the previously created variables.&lt;/li&gt;
&lt;li&gt;Finally, we’ll filter out &lt;code&gt;null&lt;/code&gt; elements and return the array.
This array is what we’ll iterate through to get the list of items in our pagination as follows:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ol&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex gap-3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pageNumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left-ellipsis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right-ellipsis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relative top-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ellipsis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="p"&gt;...&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;              &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;setsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;posinset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`page-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
                  &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;goToPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;
                  &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;underline underline-offset-3 border-zinc-300&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`go to page &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;              &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;})}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ol&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s how to create a reusable and accessible pagination! Personally, I learned how to build pagination from scratch the hard way because I had to implement it in a live coding session. I hope my experience helps you in your career and that you can implement it and even improve it!&lt;/p&gt;

&lt;p&gt;You can follow me:&lt;br&gt;
linkedin: &lt;a href="https://www.linkedin.com/in/micaelaavigliano/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/micaelaavigliano/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Best regards,&lt;br&gt;
Mica &amp;lt;3&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
