Minimum Touch Target Size: WCAG, iOS, Android and Tailwind
Ask what the minimum touch target size is and you get four numbers: 24, 28, 44 and 48. They come from different documents, in different units, and two of them are floors that nobody should design to. This tutorial sorts out what WCAG 2.2, Apple and Android ask for, picks one number for a React web app, and builds it in Tailwind CSS v4.1: buttons, icon buttons and checkboxes that take 44px taps on a phone while the desktop keeps its 32px and 40px controls. It ends with a Playwright test that fails when a target shrinks.
What is the minimum touch target size?
The minimum touch target size is 24 by 24 CSS pixels for WCAG 2.2 level AA, 44 by 44 for WCAG level AAA and for Apple's default iOS control, and 48 by 48 dp for Material and Android. The rest of each document is about exceptions and spacing:
The units are closer than they look. With the width=device-width viewport, which the Next.js App Router adds by default, one CSS pixel is one point on an iPhone and one dp on Android. So 44px in your CSS is Apple's 44 pt, and 48px is Material's 48 dp.
Two details matter more than the numbers. WCAG measures the area that accepts the tap, which it defines as the "region of the display that will accept a pointer action", so padding counts and a small icon can still pass. And WCAG covers every pointer, mouse included, so the 24px floor applies to your desktop layout too.
What does WCAG 2.2 require for touch targets?
The minimum touch target size WCAG 2.2 sets at level AA is 24 by 24 CSS pixels, unless one of five exceptions applies. The spec (opens in a new tab) lists them:
- Spacing. An undersized target passes if a 24px circle centered on its bounding box does not intersect another target or the circle of another undersized target. In practice: undersized targets need their centers at least 24px apart, and at least 12px from the edge of any other target.
- Equivalent. Another control on the same page does the same thing and meets the size.
- Inline. The target sits in a sentence, like a link in a paragraph.
- User agent control. The browser sizes the control and you did not restyle it.
- Essential. That presentation is essential or legally required.
Level AAA (2.5.5) asks for 44 by 44 with the same exceptions minus spacing. WCAG's definition of a target adds a note that matters for the techniques below: where targets overlap, the overlapping area does not count toward their size, unless they perform the same action or open the same page.
Treat AA as an audit floor. It lets a 24px icon pass, and Apple and Google ask for about twice that on touch.
What is the minimum touch target size iOS and Android recommend?
Apple lists 44 by 44 points as the default control size on iOS and iPadOS and 28 by 28 points as the minimum. Google's Android guidance asks for 48 by 48 dp with at least 8 dp between targets, which it puts at about 9 mm.
Apple also says to "consider spacing between controls as important as size" and suggests about 12 points of padding around controls with a bezel and about 24 around controls without one. Native Android does part of the work for you: the Compose reference (opens in a new tab) calls 48 dp "the Material recommended minimum size" and says the system expands touch targets at the input layer. On the web, build that area yourself, in CSS.
Which size should a React web app use?
Use 44px on coarse pointers, keep 8px between neighboring targets, never go below 24px for any pointer, and give the main action of a phone screen 48px. A 44px touch target meets WCAG AAA and Apple's default in one number. If your analytics say most of your users are on Android, swap 44 for 48: pointer-coarse:h-12 on buttons and 48px in the hit-area utility below.
The desktop is where teams overcorrect. Apple's own default for a macOS control is 28 points, and 44px controls everywhere make a settings form or a table toolbar look like a kiosk. Size by input device instead of by screen width.
How do you hit 44px on touch without bloating the desktop UI?
Switch sizes on the pointer coarse media query, @media (pointer: coarse), which matches when the primary pointer is a finger. Tailwind CSS v4.1 added it as the pointer-coarse variant (opens in a new tab). A width breakpoint gets this wrong twice: an iPad in landscape is wider than lg and gets mouse-sized controls, and a narrow desktop window gets thumb-sized ones.
There are three techniques, in order of preference.
Grow the control when the layout has room
A text button can afford 4 extra pixels of height. This is the size map of the Button in Wingo UI:
The default md is 40px with a mouse and 44px under a finger, and so is the square icon size. lg is 48px everywhere, which suits a full-width submit button at the bottom of a phone screen. Since the class is a media query, the server-rendered HTML is already right on the first paint.
Add an invisible hit area when the control must look small
A 32px icon in a toolbar should stay 32px. Give it a pseudo-element that extends past its edges on touch: (44 - 32) / 2 = 6px on each side, which is -inset-1.5. That is what sm and icon-sm do above; sm only extends vertically, because a text button is usually wider than 44px already. In Tailwind v4 the after: variant sets content for you and the button is already relative (its ::before holds the hover tint).
For your own components, one utility can do the math for any size:
The percentages resolve against the control itself, and min(0px, ...) never shrinks a control that is already big enough. We measured it in Chromium: a 16px and a 32px button both take taps on exactly 44 by 44, an 80 by 32 button on 80 by 44, and a 60px button keeps its 60px.
Hit areas fail in three ways, all invisible until someone misses a tap:
- Clipping.
overflow-hiddenon the control cuts its own::after, and an ancestor withoverflow-hiddencuts it at the ancestor's edge. Put clipping on an inner layer instead. The Haptic Button clips its ripple and hold fill on an inner layer, so the button itself keeps its 44px area. - Overlap. Two 32px icons 4px apart have 44px areas that overlap by 8px. The later one in the DOM wins every tap in the overlap, and WCAG does not count it. Keep centers 44px apart on touch:
gap-1 pointer-coarse:gap-3for 32px icons. - No position. Without
relativeon the control, the::aftersizes itself against the nearest positioned ancestor, and that whole ancestor becomes an invisible target for this one control.
Make the label the target
For checkboxes, radios and switches, the box can stay 16 to 20px when the whole row takes the tap. The Checkbox renders the row as one <label>. On a coarse pointer the row is at least 44px tall (pointer-coarse:min-h-11) and full width, and in a group the gap between rows drops to zero while the padding moves inside each row, so a tap between two options always lands on one of them. A box without a label, in a table cell for example, gets the ::after treatment instead: the Checkbox adds 14px on each side of its 16px and 18px boxes and 12px around the 20px one.
$ npx wingo-ui@latest add checkboxThe same idea works with a native input. min-h-6 keeps the row at the 24px floor for a mouse:
Should you use pointer: coarse or any-pointer: coarse?
Use pointer: coarse for sizes in most apps. It describes the primary input, so a phone or tablet matches and a laptop does not. any-pointer: coarse (opens in a new tab) matches when at least one connected input is coarse, and MDN notes that more than one value can match at once. A laptop with a touchscreen usually has a fine primary pointer, so it matches any-pointer: coarse and not pointer: coarse.
Pick any-pointer when touch is a common second input, as on convertible laptops and touchscreen all-in-one desktops; the cost is larger controls while someone uses the trackpad. To keep that decision in one place, give it a name:
Then write touch:h-11 and touch:hit-area, and switch the definition in one line later. Wingo UI uses pointer: coarse; the useMediaQuery hook exports it as TOUCH_QUERY, so components and app code agree on what touch means.
$ npx wingo-ui@latest add use-media-queryWhen should you detect touch devices in React?
Detect touch in React only for behavior, never for size. The server cannot know the pointer, so useIsTouch() returns false on the server and during hydration. A size picked by the hook renders desktop controls first and jumps on phones; the CSS variant is right from the first paint. The mobile-first React components guide covers the same split for layout, and the SSR-safe React use mobile hook tutorial explains why these hooks return false on the server.
Use it in effects and event handlers. Autofocus is the classic case, because focusing an input on a phone opens the keyboard over half the screen. This snippet uses the free hook, installed with npx wingo-ui@latest add use-media-query:
For decisions per interaction, read event.pointerType instead of a media query, because a touchscreen laptop can send both kinds of events. The Haptic Button plays its haptic tick only for touch and pen presses; a mouse click never vibrates.
How do you test touch targets?
Run axe for the 24px floor and a hit test at 44px with touch emulation, then try the screen with your thumb on a real phone.
axe-core's target-size rule checks WCAG 2.5.8. In axe-core 4.13 it is off by default and runs when you ask for the wcag22aa tag, for example new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"]) with @axe-core/playwright. It tests the AA floor, so a 32px icon button with no hit area passes it.
For 44px, check what the browser actually hits. In Chromium, Playwright's hasTouch: true makes (pointer: coarse) match and (hover: hover) stop matching, so your pointer-coarse: classes apply. document.elementFromPoint at the corners of a 44px square around each control then catches all three failures above, because hit testing includes ::after, respects overflow clipping and returns the topmost of two overlapping targets:
With Playwright 1.63 the Checkbox demo and the icon-sm Button pass it, while a bare 32px button and one inside an overflow-hidden wrapper are both reported.
Emulation proves the CSS. It does not prove the layout works one-handed, so finish on a phone: reach for the top corners with the hand that holds it, and tap the controls you placed close together.
Where should you start?
Install the free Button with npx wingo-ui@latest add button and open your app on a phone: every md button is 44px there and 40px with a mouse, and the small sizes carry their own hit area. Add the Checkbox for forms, and copy the hit-area utility for anything you built yourself. The Haptic Button, with the same size map plus press feedback and hold to confirm, is part of Wingo UI Pro. For the rest of the phone rules, from bottom sheets to safe areas, see Mobile UI in React.
FAQ
What is the minimum touch target size in WCAG 2.2?
Level AA (SC 2.5.8) asks for 24 by 24 CSS pixels, with exceptions for small targets that are spaced far enough apart, links inside a sentence, unstyled browser controls, controls with an equivalent elsewhere on the page and essential presentations. Level AAA (SC 2.5.5) asks for 44 by 44 CSS pixels.
What is the minimum touch target size on iOS?
As of October 2026, Apple's Human Interface Guidelines list 44 by 44 points as the default control size on iOS and iPadOS and 28 by 28 points as the minimum. In a mobile browser with a width=device-width viewport, one CSS pixel is one point, so 44px in CSS matches Apple's 44 pt.
Is a 44px touch target enough for Android?
It meets WCAG AAA and Apple's default, and it is 4px short of Android's recommendation. Google's Android accessibility guidance asks for 48 by 48 dp with 8 dp between targets, and one dp is one CSS pixel in a mobile browser. If most of your users are on Android, use 48px on coarse pointers.
Does an invisible hit area count toward the target size?
Yes. WCAG measures the region that accepts pointer input, so padding or a pseudo-element hit area counts. It stops counting where an overflow-hidden ancestor clips it or where it overlaps a neighbor that does something else.
What is the pointer coarse media query?
@media (pointer: coarse) matches when the primary pointing device has limited accuracy, such as a finger on a touchscreen. Tailwind CSS v4.1 and later ship it as the pointer-coarse variant; any-pointer: coarse matches when any connected input is coarse, which includes a laptop with a touchscreen.
Do Wingo UI components meet 44px on touch screens?
The Button, Checkbox and Haptic Button do. Medium buttons grow from 40 to 44px under pointer: coarse, small ones keep their look and gain a 44px hit area, and checkbox rows are at least 44px tall with the whole row as the target. The Button and Checkbox are free; the Haptic Button is Pro.
- Mobile UI
- Accessibility
- Tailwind CSS
- React