From dc8c85a65041a29c51dac4b99a9f655637977657 Mon Sep 17 00:00:00 2001 From: AlexandraGallipoliRodrigues Date: Wed, 3 Jun 2026 11:53:28 +0200 Subject: [PATCH] fix(Button): enforce 48px minimum touchable area on touchable devices (WCAG 2.5.5) Buttons and button links did not guarantee the minimum touch target size required by WCAG 2.5.5 / ABNT-ANATEL. Short button links (eg. "Fazer recarga" in a card) and small buttons fell below the minimum, failing the Android Accessibility Scanner. Following the chip/switch-component pattern, add a transparent `::after` overlay that enforces a 48px minimum interactive area ONLY on touchable devices. The overlay is absolutely positioned and overflows the button when it's smaller than 48px, so it does NOT affect the layout nor the visible button size (per design spec #2548 / DSNCORE-2286). It self-limits via min(0px, ...), so default buttons (already >=48px) are untouched. Because the overlay must not be clipped, the button's `overflow: hidden` (used to clip the loading animation) is moved to an inner content wrapper. Co-Authored-By: Claude Opus 4.8 --- src/button.css.ts | 38 +++++++++++++++++++++++++++++++++++++- src/button.tsx | 4 ++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/button.css.ts b/src/button.css.ts index 20a0c74eae..b1ef649899 100644 --- a/src/button.css.ts +++ b/src/button.css.ts @@ -22,6 +22,13 @@ export const linkMinWidth = { small: '24px', }; +/** + * Minimum touchable area (48px) enforced ONLY on touchable devices. It's painted as a + * transparent `::after` overlay so it does NOT affect the layout nor the visible button size + * (same pattern as chip/switch-component). See DSNCORE-2286 / spec #2548 and WCAG 2.5.5. + */ +const MIN_TOUCHABLE_AREA = 48; + export const borderSize = '1.5px'; export const iconMargin = '8px'; export const chevronMarginLeft = '2px'; @@ -61,7 +68,6 @@ const button = style([ position: 'relative', width: 'auto', borderRadius: vars.borderRadii.button, - overflow: 'hidden', padding: 0, }), { @@ -75,6 +81,20 @@ const button = style([ '@media': { [mq.touchableOnly]: { transition: 'none', + /** + * Force a minimum 48px touchable area on touchable devices without affecting the + * layout (transparent overlay that overflows the button when it's smaller than 48px). + * min() is not supported in old browsers (https://caniuse.com/css-math-functions); + * in that case we fall back to 0 and don't enforce the area. + */ + '::after': { + content: '', + position: 'absolute', + top: [0, `min(0px, calc((100% - ${MIN_TOUCHABLE_AREA}px) / 2))`], + bottom: [0, `min(0px, calc((100% - ${MIN_TOUCHABLE_AREA}px) / 2))`], + left: [0, `min(0px, calc((100% - ${MIN_TOUCHABLE_AREA}px) / 2))`], + right: [0, `min(0px, calc((100% - ${MIN_TOUCHABLE_AREA}px) / 2))`], + }, }, }, }, @@ -83,6 +103,22 @@ const button = style([ export const small = style({}); export const smallLink = style({}); +/** + * Wraps the button content and keeps the `overflow: hidden` that used to be on the button itself. + * It's needed because the button can no longer clip its overflow (that would also clip the + * `::after` touchable-area overlay), but we still want to clip the loading content animation. + */ +export const contentWrapper = style([ + sprinkles({ + display: 'block', + position: 'relative', + overflow: 'hidden', + }), + { + borderRadius: 'inherit', + }, +]); + export const loadingFiller = style([ sprinkles({ display: 'block', diff --git a/src/button.tsx b/src/button.tsx index 13b445c601..770f0282dc 100644 --- a/src/button.tsx +++ b/src/button.tsx @@ -173,7 +173,7 @@ const renderButtonContent = ({ }); return ( - <> +
{/* text content */}
{StartIcon && ( @@ -265,7 +265,7 @@ const renderButtonContent = ({ ) : null}
- +
); };