DEV Community

Cover image for Making a custom input counter component accessible
Maya Shavin 🌷☕️🏡
Maya Shavin 🌷☕️🏡

Posted on • Edited on • Originally published at mayashavin.com

7 2 2 1

Making a custom input counter component accessible

This blog post is not a tutorial on how to build an input counter component, but rather a discussion on how to structure the component for accessibility and good practice.

The problem of overlapping elements

Counter component

We recently encountered an input counter component that failed the accessibility test performed by Evinced Web Flow Analyzer. The reported error was "There is an overlapping between interactive elements" involving the increment button and the input field. This issue surprised us because, at first glance, the component appeared accessible, with keyboard navigation, voiceover, and focus state working as expected during manual testing.

To investigate, we examined the component's implementation. Initially, its HTML template seemed well-structured, with the increment button, input field, and decrement button nested as sibling elements within a div container:

<div class="container">
  <button aria-label="Increment value" class="btn incr">+</button>
  <input type="number" class="input" aria-label="Counter value" class="counter" />
  <button aria-label="Decrement value" class="btn decr">-</button>
</div>
Enter fullscreen mode Exit fullscreen mode

However, the component's CSS styling revealed a different story. The increment and decrement buttons were absolutely positioned, while the input field was styled with width: calc(100% - 60px) and horizontal padding (padding-inline: 30px), aligning with the button widths:

.container {
  /**... */
  width: 150px;
  position: relative;
}

.btn {
  position:absolute;
  height: 100%;
  min-width: 30px;
}

.incr {
  left: 1px;
}

.decr {
  right: 0;
}

.counter {
  border:none;
  padding: 5px;
  padding-inline: 30px;
  width: calc(100% - 60px);
  text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

While visually effective, this CSS introduced poor positioning practices:

  1. Using position: absolute removes buttons from the normal document flow, forcing the browser into additional layout computations.
  2. Button positions aren't relative to the input field, causing additional complexity in CSS rules when adjusting font sizes, widths, or container dimensions.
  3. Absolute positioning complicates RTL/LTR language support (such as Arabic or Hebrew), requiring extra CSS adjustments.
  4. There is no actual necessity for absolute positioning since elements are already correctly structured in HTML.

Additionally, the left: 1px rule caused a 1px overlap of the increment button on the input field, directly triggering the accessibility error.

Fixing overlapping and RTL issues using CSS Flexbox

A simple fix would be adjusting left: 1px to left: 0, removing the immediate overlap. However, this would only patch the bug without addressing the root issue (position: absolute).

A better solution is using CSS Flexbox for the container, eliminating absolute positioning entirely. The updated CSS rules remove unnecessary properties (left, right, height: 100%, padding, and calc()):

.container {
  border: 1px solid lightgray;
  padding:0;
  display: flex;
}

.btn {
  min-width: 30px;
}

input {
  border:none;
  padding: 5px;
  text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

However, applying Flexbox introduces an overflow issue for the input field, as form elements inherently have a default width set by browser stylesheets.

Counter component with an overflowed input

Resolving input overflow in Flexbox

To address the overflow, set min-width: 0 and flex: 1 on the input field. This ensures it dynamically shrinks or grows within the available container space:

input {
  /** ... */
  min-width: 0;
  flex: 1;
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, using a fieldset instead of a div container could omit the min-width: 0 property. This provides a semantically meaningful solution beneficial for accessibility.

Supporting RTL/LTR languages

With Flexbox, the component inherently supports RTL/LTR languages without additional CSS rules. To verify this, use the dir attribute on the container:

<div class="container" dir="rtl">
  <button class="increment" aria-label="Increment value" class="btn incr">+</button>
  <input type="number" class="input" aria-label="Counter value" class="counter" />
  <button class="decrement" aria-label="Decrement value" class="btn decr">-</button>
</div>
Enter fullscreen mode Exit fullscreen mode

This approach automatically swaps the increment and decrement buttons appropriately, maintaining a centered input field.


Summary

In this blog post, we addressed accessibility issues caused by overlapping elements in an input counter component due to absolute positioning. By replacing this with CSS Flexbox and adjusting the CSS style of input field, we significantly improved the component's accessibility and flexibility, providing RTL/LTR language support out of the box.

👉 Learn about Vue 3 and TypeScript with my new book Learning Vue!

👉 Follow me on X | LinkedIn.

Like this post or find it helpful? Share it 👇🏼 😉

Image of Stellar post

How a Hackathon Win Led to My Startup Getting Funded

In this episode, you'll see:

  • The hackathon wins that sparked the journey.
  • The moment José and Joseph decided to go all-in.
  • Building a working prototype on Stellar.
  • Using the PassKeys feature of Soroban.
  • Getting funded via the Stellar Community Fund.

Watch the video 🎥

Top comments (5)

Collapse
 
ravijhabit profile image
Ravi Shankar Jha

Great article @mayashavin 👏

I have 2 questions:
Why are we using two class attribute ?
How does input and inc & dec button gets linked ? (I have an assumption here that accessiblity should link increment and decrement buttons with the input. Please let me know if my assumption is wrong)

Collapse
 
mayashavin profile image
Maya Shavin 🌷☕️🏡

If you use absolute position approach then you will need two class attributes, one for increment and one for decrement buttons. Otherwise, one class “btn” can be enough for both.
Not sure about the linking thing, can you elaborate what you mean?

Collapse
 
ravijhabit profile image
Ravi Shankar Jha
<div class="container">
  <button class="increment" aria-label="Increment value" class="btn incr">+</button>
  <input type="number" class="input" aria-label="Counter value" class="counter" />
  <button class="decrement" aria-label="Decrement value" class="btn decr">-</button>
</div>
Enter fullscreen mode Exit fullscreen mode

This is code snippet from your blog. Here two class attribute is used. Probably first one doesnt have any significance. Please update the code snippet. (Earlier I thought it would be vue.js functionality).

Second question:
I have an assumption. So, please share your opinion on my assumption and if it feels right then move to the question part.
Assumption: For Accessiblity the input and buttons should be linked signifying the relation between input and buttons to the screen readers.

Question: Only if above assumption is true, How is this linking of input and buttons take place ?

Thread Thread
 
mayashavin profile image
Maya Shavin 🌷☕️🏡

the first class attribute doesn't do anything, it was a typo, and browser won't take it afaik. But good catch on that. Will update my snippet.

You can link input and buttons using WAI-ARIA attributes, and that's a different blog post, to give more context for the screen readers.

Collapse
 
nevodavid profile image
Nevo David

pretty cool seeing fixes that make stuff actually better for everyone - you think keeping things accessible like this really comes down to paying more attention to the basics or is it more about sticking with the newest tools