Skip to main content

Increase tap target size and spacing for mobile usability

Small tap targets force customers to zoom in and tap carefully, which increases error rates. Appropriately sized touch targets, at a minimum of 48 × 48 px, make a storefront easier to use on a touchscreen and help you meet accessibility requirements.


Customers interact with touchscreens using fingers, which are less precise than mouse cursors. When tap targets are too small or too close together, customers make errors and take longer to complete tasks.

Small tap targets create specific problems:

  • Mis-taps: customers accidentally tap the wrong elements, and then have to undo the mistake and try again.
  • Extra interactions: every correction is another tap that the page has to handle, so a mis-tap-prone layout does more work than it needs to.
  • Zoom requirements: customers have to zoom in to tap accurately, which interrupts the task.
  • Accessibility barriers: customers with motor impairments struggle with small targets.
  • Abandonment: frustration leads to task abandonment and site exits.

A fingertip is much less precise than a cursor. Ten millimeters is about 38 CSS px at the reference resolution of 96 px per inch, and the major platform and accessibility guidelines all round up from a figure in that range.

There are three commonly cited target sizes, and they aren't interchangeable:

  • WCAG 2.2 SC 2.5.8 Target Size (Minimum): 24 × 24 CSS px, at Level AA. This is the criterion that most conformance targets require. It has exceptions for spacing, inline targets, targets whose size is essential, and targets with an equivalent alternative elsewhere on the page.
  • WCAG 2.1 SC 2.5.5 Target Size: 44 × 44 CSS px, at Level AAA. Apple's Human Interface Guidelines use the same 44 × 44 pt figure.
  • Material Design: 48 × 48 dp, which is a platform guideline rather than a conformance requirement.

The 48 × 48 px recommendation on this page satisfies all three.


Anchor to Set minimum tap target sizesSet minimum tap target sizes

Use a minimum of 48 × 48 px for interactive elements:

/* Buttons */
button,
.button,
input[type="submit"],
input[type="button"] {
min-height: 48px;
min-width: 48px;
padding: 12px 24px;
font-size: 16px;
}

/* Links in navigation */
.site-nav a {
min-height: 48px;
padding: 12px 16px;
display: flex;
align-items: center;
}

/* Icon buttons */
.icon-button {
min-height: 48px;
min-width: 48px;
padding: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
}

/* Form inputs */
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
select {
min-height: 48px;
padding: 12px;
font-size: 16px;
}

/* Checkboxes and radio buttons */
input[type="checkbox"],
input[type="radio"] {
min-height: 24px;
min-width: 24px;
margin: 12px; /* Separates neighboring targets. Margin isn't part of the hit area. */
}

/* Enlarge the hit area itself by making the label the target */
.checkbox-field label {
display: flex;
align-items: center;
gap: 12px;
min-height: 48px;
}

Anchor to Add adequate spacing between targetsAdd adequate spacing between targets

Use a minimum of 8 px spacing between interactive elements:

/* Button groups */
.button-group {
display: flex;
gap: 12px; /* Space between buttons */
flex-wrap: wrap;
}

/* Navigation links */
.site-nav {
display: flex;
gap: 8px; /* Minimum spacing */
}

/* Product grid */
.product-grid {
display: grid;
gap: 16px; /* Comfortable spacing for tap targets */
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}

/* Form fields */
.form-field {
margin-bottom: 20px; /* Space between fields */
}

Anchor to Increase clickable area with paddingIncrease clickable area with padding

Expand tap area beyond visible element:

/* Small icon with a large tap area */
.close-button {
position: relative;
width: 24px;
height: 24px;
}

.close-button::before {
content: '';
position: absolute;
top: -12px;
left: -12px;
right: -12px;
bottom: -12px;
/* Creates a 48x48px tap area around a 24x24px icon */
}

/* Alternative: use padding */
.icon-link {
display: inline-flex;
padding: 12px; /* Adds 12px on each side, so a 24px icon becomes a 48x48px target */
}

.icon-link svg {
width: 24px;
height: 24px;
}

Anchor to Make entire card or row tappableMake entire card or row tappable

Expand clickable area to container:

{%- comment -%}
Make the entire product card tappable.
{%- endcomment -%}

<a href="{{ product.url }}" class="product-card">
<img src="{{ product.featured_image | image_url: width: 400 }}" alt="{{ product.title }}">
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</a>

<style>
.product-card {
display: block;
padding: 16px;
text-decoration: none;
color: inherit;
/* The entire card is tappable */
}

.product-card:hover,
.product-card:focus {
background: #f5f5f5;
}
</style>

Stretched link pattern:

{%- comment -%}
Make the container tappable while keeping semantic HTML.
{%- endcomment -%}

<div class="blog-card">
<img src="{{ article.image | image_url: width: 400 }}" alt="{{ article.title }}">
<h3>
<a href="{{ article.url }}" class="stretched-link">{{ article.title }}</a>
</h3>
<p>{{ article.excerpt }}</p>
</div>

<style>
.blog-card {
position: relative;
padding: 16px;
}

.stretched-link::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
/* Expands the link to cover the entire card */
}
</style>

Mobile-friendly navigation:

/* Mobile menu items */
.mobile-nav a {
display: block;
min-height: 48px;
padding: 12px 20px;
font-size: 16px;
text-decoration: none;
color: #333;
border-bottom: 1px solid #eee;
}

.mobile-nav a:active {
background: #f5f5f5;
}

/* Hamburger menu button */
.menu-toggle {
min-width: 48px;
min-height: 48px;
padding: 12px;
background: transparent;
border: none;
cursor: pointer;
}

Anchor to Product variant selectorsProduct variant selectors

Large, tappable variant options:

{%- comment -%}
Color swatches with adequate size and spacing.
{%- endcomment -%}

<div class="variant-selector">
<label>Color</label>
<div class="color-swatches">
{% for option in product.options_with_values %}
{% if option.name == 'Color' %}
{% for value in option.values %}
<input
type="radio"
name="color"
id="color-{{ value | handle }}"
value="{{ value }}"
class="swatch-input"
>
<label for="color-{{ value | handle }}" class="swatch-label">
<span class="swatch" style="background-color: {{ value | downcase }}"></span>
<span class="swatch-name">{{ value }}</span>
</label>
{% endfor %}
{% endif %}
{% endfor %}
</div>
</div>

<style>
.color-swatches {
display: flex;
flex-wrap: wrap;
gap: 12px;
}

.swatch-input {
position: absolute;
opacity: 0;
pointer-events: none;
}

.swatch-label {
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
min-height: 60px;
padding: 8px;
cursor: pointer;
border: 2px solid transparent;
border-radius: 4px;
}

.swatch {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #ddd;
}

.swatch-name {
font-size: 12px;
margin-top: 4px;
}

.swatch-input:checked + .swatch-label {
border-color: #0066cc;
}

/* The input is visually hidden, so the label carries the focus indicator */
.swatch-input:focus-visible + .swatch-label {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
</style>

Anchor to Mobile navigation with proper tap targetsMobile navigation with proper tap targets

<nav class="mobile-nav">
<button class="menu-toggle" aria-label="Toggle menu">
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</button>

<ul class="nav-menu">
{% for link in linklists.main-menu.links %}
<li>
<a href="{{ link.url }}">{{ link.title }}</a>
</li>
{% endfor %}
</ul>
</nav>

<style>
.menu-toggle {
min-width: 48px;
min-height: 48px;
padding: 12px;
background: transparent;
border: none;
display: flex;
align-items: center;
justify-content: center;
}

.menu-toggle:active {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.nav-menu {
list-style: none;
margin: 0;
padding: 0;
}

.nav-menu a {
display: block;
min-height: 48px;
padding: 14px 20px;
font-size: 16px;
color: #333;
text-decoration: none;
border-bottom: 1px solid #eee;
}

.nav-menu a:active {
background: #f5f5f5;
}
</style>

Anchor to Product quantity selector with large tap targetsProduct quantity selector with large tap targets

<div class="quantity-selector">
<label for="quantity">Quantity</label>
<div class="quantity-controls">
<button type="button" class="quantity-button" onclick="decreaseQuantity()">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M2 8h12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>

<input
type="number"
id="quantity"
name="quantity"
value="1"
min="1"
class="quantity-input"
>

<button type="button" class="quantity-button" onclick="increaseQuantity()">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>

<style>
.quantity-controls {
display: flex;
align-items: center;
gap: 8px;
}

.quantity-button {
min-width: 48px;
min-height: 48px;
padding: 12px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}

.quantity-button:active {
background: #e5e5e5;
}

.quantity-input {
min-height: 48px;
width: 60px;
padding: 12px;
font-size: 16px;
text-align: center;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>

<script>
function decreaseQuantity() {
const input = document.getElementById('quantity');
const value = parseInt(input.value);
if (value > 1) {
input.value = value - 1;
}
}

function increaseQuantity() {
const input = document.getElementById('quantity');
input.value = parseInt(input.value) + 1;
}
</script>

  1. Touch target testing: use Chrome DevTools Device Mode. Enable Show rulers to measure tap targets, verify that all interactive elements are at least 48 × 48 px, and check spacing between targets (at least 8 px).

  2. Real device testing: test on actual mobile devices. Try tapping all interactive elements, verify that no mis-taps occur, and check that tapping feels comfortable.

  3. Accessibility testing: confirm that every target meets WCAG 2.2 SC 2.5.8 (24 × 24 CSS px, Level AA), or one of its exceptions. If you're targeting Level AAA, then check SC 2.5.5 (44 × 44 CSS px) as well. Verify that keyboard navigation reaches every target and that each one has a visible focus indicator.

  4. Automated testing: run the Lighthouse accessibility category and check the Touch targets do not have sufficient size or spacing (target-size) audit. Lighthouse 12.0.0 replaced the older SEO tap-targets audit with this accessibility audit, so an older report shows a different audit name.



Was this page helpful?