惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
P
Privacy International News Feed
Security Latest
Security Latest
H
Hacker News: Front Page
T
Tenable Blog
The Hacker News
The Hacker News
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
Project Zero
Project Zero
O
OpenAI News
AI
AI
Spread Privacy
Spread Privacy
C
CERT Recently Published Vulnerability Notes
The Last Watchdog
The Last Watchdog
G
GRAHAM CLULEY
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Scott Helme
Scott Helme
Application and Cybersecurity Blog
Application and Cybersecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
NISL@THU
NISL@THU
A
Arctic Wolf
T
Threat Research - Cisco Blogs
PCI Perspectives
PCI Perspectives
N
News and Events Feed by Topic
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
Simon Willison's Weblog
Simon Willison's Weblog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
Google Online Security Blog
Google Online Security Blog
罗磊的独立博客
L
LINUX DO - 最新话题
U
Unit 42
S
Security Affairs
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
博客园 - 【当耐特】
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
月光博客
月光博客
Engineering at Meta
Engineering at Meta
腾讯CDC
F
Full Disclosure
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Recorded Future
Recorded Future
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 司徒正美
The Cloudflare Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Accessible and Functional Quantity Spinbutton Pattern
Mica · 2026-05-13 · via DEV Community

Mica

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: Is there anybody out there?

Introduction

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 quantity spinbutton pattern and from there, explore different variations depending on the use case.

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

Expected behaviors

Keyboard

Key Action
Up Arrow Increases the value by its step value
Down Arrow Decreases the value by its step value
Home If the spinbutton has a minimum value, sets the value to its minimum
End If the spinbutton has a maximum value, sets the value to its maximum
Command or Fn + Right Arrow Substitute for Apple's keyboards that do not have a End key to set the value to its maximum
Command or Fn + Left Arrow Substitute for Apple's keyboards that do not have a End key to set the value to its minimum

Screen Readers

NVDA Navigation

You can download NVDA here and it's only available for the Windows operating system. It is compatible with Chrome, Firefox and Edge browsers.
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:

  • Browse Mode: Browse mode is used when reading documents or web pages.
  • Focus Mode: Focus mode is used when the user enters a form or other fields that require user input.

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

VoiceOver Navigation

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

Command Task
Tab/Shift + Tab Go to next/previous focusable item (link, button, input, etc.)
VO + Space Bar Activate a link or form control
VO + Right Arrow Move VO cursor to next element (not limited to focusable items)
VO + Left Arrow Move VO cursor to previous element
VO + Shift + Down Arrow Once the VO cursor is on the spinbutton, this combination enters the spinbutton
Up Arrow Once inside the spinbutton, increment by step
Down Arrow Once inside the spinbutton, decrement by step
Home Jump to aria-valuemin (via your custom handler)
End Jump to aria-valuemax (via your custom handler)

Gestures VoiceOver - iOS

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.

Gesture Task
Swipe next Read next item
Swipe left Read previous item
Double-tap Activate (link, button)
Swipe up If the VO cursor is over the spinbutton, increase the value
Swipe down If the VO cursor is over the spinbutton, decrease the value

Gestures Talkback - Android

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.

Gesture Task
Swipe next Read next item
Swipe left Read previous item
Double-tap Activate (link, button)
Swipe up If the VO cursor is over the spinbutton, increase the value
Swipe down If the VO cursor is over the spinbutton, decrease the value

Pattern A: APG (ARIA Authoring Practices Guide) Spinbutton by W3C

Currently everyone is following the APG (ARIA Authoring Practices Guide) as if it were the bible and not as what it is: a guide, a very useful guide, btw, THE guide. Everything written there is considered a commandment written in stone by non-accessibility experts (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 APG Spin Button Pattern, the first thing you see is an alert with the following warning:

The code in this example is not intended for production environments. Before using it for any purpose, read this to understand why.

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

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.

So, what does this mean? Well, a couple of important things:

  1. Always be critical of everything (a rule for life, tho)
  2. 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.
  3. There is always (well, almost 80% always) an HTML equivalent.

After this brief introduction to the APG, I would like to dive into its Spinbutton pattern. W3C (The World Wide Web Consortium) presents us a solution using the WAI-ARIA role="spinbutton" on the input 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 tabindex="-1" but they are still operable via mouse and by using screen readers. Only the tag with the role="spinbutton" 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.

In the accessibility field we have a mantra: No ARIA is better than Bad ARIA. 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, WebAIM org launched its annual report for 2026 about the state of accessibility in over 1 million pages: The WebAIM Million. 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:

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.

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.

Functional Example – APG

you can find the functional example here

APG code

<input 
  id="add-to-bag"
  role="spinbutton"
  type="text"
  inputMode="numeric"
  pattern="[0-9]*"
  autoComplete="off"
  spellCheck="false"
  aria-valuemin={MIN}
  aria-valuemax={MAX}
  aria-valuenow={isValid ? numeric : undefined}
  aria-valuetext={value}
  aria-invalid={!isValid || undefined}
  aria-describedby={isValid ? 'help-add-to-bag' : 'help-add-to-bag error-add-to-bag'}
  value={value}
  onChange={handleChange}
  onKeyDown={handleKeyDown}
/>

Enter fullscreen mode Exit fullscreen mode

  • id: to relate the <input /> with the <label> element
  • role="spinbutton": this is a WAI-ARIA role that allows the user to increase and decrease a value within a given range. This role can be used on <input> or on a non-semantic element such as <div>. The APG pattern for the spinbutton component proposes the <input> with a type="text" making the development more challenging. The expected keyboard behavior for a spinbutton 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 role="spinbutton" or of the <input type="text" />, we have to programmatically cover these behaviors. The spinbutton 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 tabIndex={-1}.
  • type="text": implies that the spinbutton 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.
  • inputMode="numeric": 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 numeric the virtual keyboard display to enter a value will be a QWERTY instead of a numeric input keyboard.
  • pattern="[0-9]*": again, since the input is type="text", 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).
  • autocomplete="off": 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).
  • aria-valuemin="[NUMBER]": sets the minimum value allowed for the spinbutton. The default is 0, if no value is passed.
  • aria-valuemax="[NUMBER]": sets the maximum value allowed for the spinbutton. The default is 100, if no value is passed.
  • aria-valuenow="[number]": 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 spinbutton 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 spinbutton 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 APG example.
  • aria-invalid="[boolean]": indicates that the current value is invalid.
  • aria-describedby="[IDREF]": this attribute is used to establish a relationship between widgets or groups and the text that describes them.

Conclusion for the APG Spinbutton Pattern

To articulate a conclusion for this pattern, I drew on Core Accessibility API Mappings 1.2 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.

Since a spinbutton 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 spinbutton 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.
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.

Pattern B: Native HTML solution

Unfortunately, nothing is perfect; the native HTML solution for spinbutton is tempting because it works well in most cases BUT has a few limitations that aren't HTML-related but browser-related, buuuh.

Browsers have different engines to create what we colloquially know as

Accessibility Tree

. This tree is just an object with

nodes

. 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.

  • Blink: this is the browser engine for Chrome, Edge, Opera and Brave (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:

Chrome DevTools accessibility node showing the computed properties for a spinbutton: role spinbutton, name
Supported on the following Accessibility APIs:

  • Windows: IAccessible, IAccessible2 and UIAutomation
  • Mac: 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)
  • Linux: ATK
  • Android: AccessibilityNodeInfo and AccessibilityNodeProvider. I opened a ticket for this operating system because it is not exposing the spinbutton correctly preventing the user from having an accessible experience. The ticket to Android can be seen here
    • Gecko: this is the browser engine for Firefox. The Accessibility Tree it creates, in my opinion, is the most complete and it is compatible with the following Accessibility APIs:
  • Windows: IAccessible, IAccessible2 and UIAutomation
  • Mac: mozAccessible
  • Linux: ATK
    • WebKit: this is the browser engine for Safari in macOS and iOS. Here is where our problems start. The engine does not recognize the element as a spinbutton but as a role="textbox" if we don't explicitly pass the role="spinbutton" to the <input type="text" />. 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. 'Safari Web Inspector accessibility panel showing: Role textbox (default), Label Again, I ended up opening a ticket to Apple about this behavior. The ticket to Apple can be seen here

Functional Example – HTML

The native functional example can be found here

HTML native code

<input 
  id="add-to-bag-native"
  type="number"
  role="spinbutton"
  autoComplete="off"
  min={MIN}
  max={MAX}
  step={STEP}
  value={value}
/>

Enter fullscreen mode Exit fullscreen mode

  • id: same thing as in the APG pattern
  • type="number": this is what natively converts an input into a spinbutton 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 type="number" (see/read/perceive the image below), pressing Up Arrow or Down Arrow, or swiping (on mobile with the screen readers on).

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
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.

  input[type="number"] {
    appearance: textfield;
  }

  input[type="number"]::-webkit-outer-spin-button,
  input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

Enter fullscreen mode Exit fullscreen mode

  • appearance: textfield;, we need it to strip the native spinbutton styling in Firefox
  • ::-webkit-outer-spin-button and ::-webkit-inner-spin-button are pseudo-selectors that only Blink and WebKit recognize and the -webkit-appearance: none property strips the native spinbutton styling in Chrome and Safari.
    • role="spinbutton": well, having in mind all the compatibility issues we have in different browsers, I found out that if you only pass the WAI-ARIA role="spinbutton" 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.
    • autoComplete="off": just the same as in the APG example
    • min={NUMBER}: 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.
    • max={NUMBER}: the maximum value to accept for this input and should be greater than or equal to the minimum value.
    • step={NUMBER}: defines the allowed interval between valid values. The default is 1 and only accepts integers.
    • value={NUMBER}: a number representing the value of the number entered into the input

Conclusion for the HTML native solution

If you want to have a functional, accessible and widely supported across browsers spinbutton 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 role="spinbutton" to force its semantic value on some browser engines was tested on:

  • Chrome (version 148) + macOS (Tahoe version 26.4.1) + VoiceOver
  • Chrome (version 148) + iOS (version 26.4.1)/ iPhone pro 15 + VoiceOver
  • Chrome (version 148) + Windows 11 + NVDA
  • Chrome (version 148) + Android/Pixel 10 (version 16) + TalkBack
  • Firefox (version 150) + Windows 11 + NVDA
  • Firefox (version 150) + macOS (Tahoe version 26.4.1)
  • Safari (version 26.4) + macOS (Tahoe version 26.4.1) + VoiceOver
  • Safari (version 26.4) + iOS (version 26.4.1) + VoiceOver

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.

  const handleKeyDown = (ev: KeyboardEvent<HTMLInputElement>) => {
    if (ev.key === 'End' || (ev.metaKey && ev.key === 'ArrowRight')) {
      ev.preventDefault()
      setValue(MAX)
    } else if (ev.key === 'Home' || (ev.metaKey && ev.key === 'ArrowLeft')) {
      ev.preventDefault()
      setValue(MIN)
    }
  }

Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Having a semantic, functional and accessible spinbutton 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.
According to WHO 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.

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.

You can find me on: