Ver contenido
Introducción
Muchos usuarios con baja visión necesitan agrandar el texto para leer cómodamente. WCAG 1.4.4 (Redimensionar Texto) requiere que el texto pueda redimensionarse hasta un 200% sin perder contenido o funcionalidad. Esto significa que tu sitio web debe permanecer usable cuando los usuarios hacen zoom usando controles del navegador o tecnología asistiva.
Cuando los sitios web usan tamaños fijos en píxeles o no consideran el escalado del texto, los usuarios pueden encontrar contenido cortado, texto superpuesto o diseños rotos que hacen el sitio inutilizable con tamaños de texto más grandes.
Qué Requiere WCAG
1.4.4 Redimensionar Texto (Nivel AA)
El texto puede redimensionarse sin tecnología asistiva hasta un 200 por ciento sin pérdida de contenido o funcionalidad.
Excepciones:
- Subtítulos (para video)
- Imágenes de texto
Esto significa:
- Los usuarios deben poder hacer zoom hasta 200% usando controles del navegador
- Todo el texto debe permanecer legible a este nivel de zoom
- Ningún contenido debe ser recortado, cortado u oculto
- Toda la funcionalidad debe permanecer operable
- Los usuarios no deberían necesitar desplazarse horizontalmente en viewports estándar
Quién Se Beneficia
| Tipo de Usuario | Beneficio |
|---|---|
| Usuarios con baja visión | Pueden agrandar el texto a un tamaño cómodo de lectura |
| Usuarios mayores | Pueden necesitar texto más grande para legibilidad |
| Usuarios con discapacidades cognitivas | El texto más grande puede mejorar la comprensión |
| Usuarios móviles | Pueden hacer zoom en pantallas pequeñas |
| Todos los usuarios | Flexibilidad para ajustar el tamaño del texto según preferencia |
Problemas Comunes
1. Tamaños de Fuente en Píxeles Fijos
/* MAL: Los píxeles fijos no escalan con preferencias del usuario */
body {
font-size: 14px;
}
h1 {
font-size: 24px;
}
/* BIEN: Las unidades relativas escalan con configuración del usuario */
body {
font-size: 1rem; /* 16px por defecto, escala con preferencia del usuario */
}
h1 {
font-size: 1.5rem; /* 24px por defecto, escala proporcionalmente */
}
2. Contenedores de Altura Fija
/* MAL: La altura fija corta el texto agrandado */
.card-title {
height: 48px;
overflow: hidden;
}
/* BIEN: La altura flexible acomoda texto más grande */
.card-title {
min-height: 48px;
overflow: visible;
}
3. Texto en Contenedores de Ancho Fijo
/* MAL: El ancho fijo causa desbordamiento con texto más grande */
.sidebar {
width: 200px;
}
/* BIEN: Ancho flexible o diseño responsivo */
.sidebar {
width: 100%;
max-width: 200px;
}
@media (min-width: 768px) {
.sidebar {
width: 25%;
}
}
4. Unidades de Viewport para Tamaño de Fuente
/* MAL: Las unidades vw no responden al zoom solo de texto */
h1 {
font-size: 5vw;
}
/* MEJOR: Clamp con rem como respaldo */
h1 {
font-size: clamp(1.5rem, 5vw, 3rem);
}
Probando Redimensionar Texto
Prueba de Zoom del Navegador
- Abre tu sitio web en un navegador
- Usa Ctrl/Cmd + para hacer zoom al 200%
- Verifica que todo el texto sea legible
- Comprueba que ningún contenido esté cortado u oculto
- Asegura que todos los elementos interactivos sigan siendo usables
- Prueba el desplazamiento horizontal (debe ser mínimo en contenido típico)
Prueba de Zoom Solo de Texto
Algunos navegadores ofrecen zoom solo de texto:
- Firefox: Ver → Zoom → Zoom Solo Texto
- Esto prueba si tu diseño maneja específicamente texto más grande
Qué Verificar
- [ ] Todo el texto es legible al 200% de zoom
- [ ] Ningún texto está cortado o truncado
- [ ] Los elementos interactivos siguen siendo clicables/tocables
- [ ] Los formularios permanecen funcionales
- [ ] La navegación sigue siendo usable
- [ ] No hay desplazamiento horizontal excesivo
- [ ] Las imágenes no se superponen al texto
Buenas Prácticas
Usar Unidades Relativas
/* Tamaños de fuente con rem */
body { font-size: 1rem; }
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
p { font-size: 1rem; }
small { font-size: 0.875rem; }
/* Espaciado con em (relativo al tamaño de fuente) */
.card {
padding: 1em;
margin-bottom: 1.5em;
}
/* Altura de línea (sin unidades para mejor escalado) */
body {
line-height: 1.5;
}
Contenedores Flexibles
/* Usar min-height en lugar de altura fija */
.content-box {
min-height: 200px;
/* No: height: 200px; */
}
/* Permitir que el texto se ajuste */
.button {
white-space: normal;
/* No: white-space: nowrap; */
}
Diseño Responsivo
/* Breakpoints para diferentes tamaños de pantalla */
.container {
padding: 1rem;
}
@media (min-width: 768px) {
.container {
padding: 2rem;
}
}
/* Tipografía fluida */
h1 {
font-size: clamp(1.5rem, 4vw + 1rem, 3rem);
}
Impacto del Escalado de Texto Deficiente
Cuando el redimensionamiento de texto no es soportado:
| Problema | Impacto |
|---|---|
| Texto cortado | Los usuarios no pueden leer información importante |
| Elementos superpuestos | El contenido se vuelve ilegible |
| Elementos interactivos ocultos | Los usuarios no pueden completar tareas |
| Desplazamiento horizontal | La navegación se vuelve frustrante |
| Diseños rotos | La apariencia profesional se daña |
Herramientas para Pruebas
- DevTools del navegador: Simular niveles de zoom
- Extensión de zoom del navegador: Probar varios porcentajes de zoom
- WAVE: Identifica problemas potenciales de zoom
- axe DevTools: Reporta fallos de redimensionamiento de texto
- Pruebas manuales: Esencial para verificación completa
Resumen de Mejores Prácticas
| Hacer | No Hacer |
|---|---|
| Usar rem/em para tamaños de fuente | Usar tamaños de fuente en píxeles fijos |
| Usar min-height para contenedores | Usar alturas fijas que cortan contenido |
| Permitir que el texto se ajuste naturalmente | Forzar nowrap en texto |
| Probar regularmente al 200% de zoom | Asumir que diseños pixel-perfect escalan |
| Usar tipografía fluida | Confiar solo en unidades de viewport |
| Diseñar para flexibilidad | Diseñar solo para tamaño de texto predeterminado |
Implementación
Sistema de Unidades CSS para Texto Escalable
Entendiendo Unidades Relativas
/*
rem = relativo al tamaño de fuente del elemento raíz (html)
em = relativo al tamaño de fuente del elemento padre
% = relativo al elemento padre
vw/vh = relativo al ancho/alto del viewport
Tamaño de fuente predeterminado del navegador: 16px
1rem = 16px (por defecto)
1em = tamaño de fuente del padre
*/
/* DIMENSIONAMIENTO RAÍZ: Establecer base en html para cálculos predecibles de rem */
html {
font-size: 100%; /* Respeta configuraciones del navegador del usuario */
/* Evitar: font-size: 16px; - anula preferencias del usuario */
}
/* VALORES PREDETERMINADOS DEL BODY */
body {
font-size: 1rem; /* 16px por defecto */
line-height: 1.5; /* Sin unidades para mejor escalado */
letter-spacing: 0.01em; /* Escala con el texto */
}
Escala Tipográfica con rem
/* Escala tipográfica usando rem para dimensionamiento consistente */
:root {
/* Escala modular (ratio 1.25 - Tercera Mayor) */
--text-xs: 0.64rem; /* 10.24px */
--text-sm: 0.8rem; /* 12.8px */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
--text-xl: 1.563rem; /* 25px */
--text-2xl: 1.953rem; /* 31.25px */
--text-3xl: 2.441rem; /* 39px */
--text-4xl: 3.052rem; /* 48.83px */
}
/* Aplicar a elementos */
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
h5 { font-size: var(--text-lg); }
h6 { font-size: var(--text-base); }
p, li, td { font-size: var(--text-base); }
small, .caption { font-size: var(--text-sm); }
Tipografía Fluida con clamp()
/* Tipografía fluida que escala suavemente */
:root {
/* clamp(mínimo, preferido, máximo) */
--fluid-sm: clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem);
--fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--fluid-lg: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
--fluid-xl: clamp(1.5rem, 1.25rem + 1.25vw, 2rem);
--fluid-2xl: clamp(2rem, 1.5rem + 2.5vw, 3rem);
--fluid-3xl: clamp(2.5rem, 2rem + 2.5vw, 4rem);
}
/* Uso */
h1 {
font-size: var(--fluid-3xl);
/* Escala entre 2.5rem y 4rem basado en viewport */
/* Siempre respeta el zoom del 200% */
}
body {
font-size: var(--fluid-base);
}
Patrones de Contenedores
Contenedores de Altura Flexible
/* INCORRECTO: La altura fija corta texto en tamaños más grandes */
.card-header {
height: 60px;
overflow: hidden;
}
/* CORRECTO: Altura mínima con crecimiento */
.card-header {
min-height: 60px;
padding: 1em;
/* El contenido puede expandirse */
}
/* CORRECTO: Altura automática con padding */
.card-content {
padding: 1.5em;
/* Altura determinada por contenido */
}
Patrones de Ancho Flexible
/* Los contenedores de texto deben ser flexibles */
.text-container {
width: 100%;
max-width: 65ch; /* Longitud óptima de lectura */
padding: 1em;
}
/* Barra lateral que no se rompe con zoom */
.sidebar {
flex: 0 0 auto;
width: clamp(200px, 25%, 300px);
min-width: min-content; /* Nunca más pequeño que el contenido */
}
/* Área de contenido principal */
.main-content {
flex: 1 1 auto;
min-width: 0; /* Permite encogimiento flex */
}
Layout Grid para Zoom
/* Grid que se adapta al tamaño del contenido */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: 1.5rem;
}
/* Este grid:
- Las tarjetas tienen mínimo 280px de ancho
- O 100% si el viewport es menor a 280px
- Se reorganiza automáticamente con zoom
*/
Implementación en React
Componente de Tipografía Escalable
import React from 'react';
interface TextProps {
as?: 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'small';
size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
children: React.ReactNode;
className?: string;
}
const sizeClasses = {
xs: 'text-xs', // 0.64rem
sm: 'text-sm', // 0.8rem
base: 'text-base', // 1rem
lg: 'text-lg', // 1.25rem
xl: 'text-xl', // 1.563rem
'2xl': 'text-2xl', // 1.953rem
'3xl': 'text-3xl', // 2.441rem
'4xl': 'text-4xl', // 3.052rem
};
export function Text({
as: Component = 'p',
size = 'base',
children,
className = ''
}: TextProps) {
return (
<Component className={`${sizeClasses[size]} ${className}`}>
{children}
</Component>
);
}
// Componente de contenedor flexible
interface ContainerProps {
children: React.ReactNode;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'prose';
className?: string;
}
const maxWidthClasses = {
sm: 'max-w-sm', // 24rem
md: 'max-w-md', // 28rem
lg: 'max-w-lg', // 32rem
xl: 'max-w-xl', // 36rem
prose: 'max-w-prose' // 65ch
};
export function Container({
children,
maxWidth = 'prose',
className = ''
}: ContainerProps) {
return (
<div className={`w-full ${maxWidthClasses[maxWidth]} px-4 ${className}`}>
{children}
</div>
);
}
Implementación en Vue 3
Sistema de Tipografía
<!-- composables/useTypography.ts -->
<script setup lang="ts">
import { computed } from 'vue'
type TextSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
const sizeMap: Record<TextSize, string> = {
xs: '0.64rem',
sm: '0.8rem',
base: '1rem',
lg: '1.25rem',
xl: '1.563rem',
'2xl': '1.953rem',
'3xl': '2.441rem',
'4xl': '3.052rem'
}
export function useTypography(size: TextSize = 'base') {
const fontSize = computed(() => sizeMap[size])
const style = computed(() => ({
fontSize: fontSize.value,
lineHeight: 1.5
}))
return { fontSize, style }
}
</script>
<!-- components/ScalableText.vue -->
<script setup lang="ts">
import { computed } from 'vue'
type TextSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
interface Props {
as?: string
size?: TextSize
}
const props = withDefaults(defineProps<Props>(), {
as: 'p',
size: 'base'
})
const sizeClasses: Record<TextSize, string> = {
xs: 'text-xs',
sm: 'text-sm',
base: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl'
}
const textClass = computed(() => sizeClasses[props.size])
</script>
<template>
<component :is="as" :class="textClass">
<slot />
</component>
</template>
<style scoped>
.text-xs { font-size: 0.64rem; }
.text-sm { font-size: 0.8rem; }
.text-base { font-size: 1rem; }
.text-lg { font-size: 1.25rem; }
.text-xl { font-size: 1.563rem; }
.text-2xl { font-size: 1.953rem; }
.text-3xl { font-size: 2.441rem; }
.text-4xl { font-size: 3.052rem; }
</style>
Configuración de Tailwind CSS
Escala Tipográfica Personalizada
// tailwind.config.js
module.exports = {
theme: {
fontSize: {
'xs': ['0.64rem', { lineHeight: '1.5' }],
'sm': ['0.8rem', { lineHeight: '1.5' }],
'base': ['1rem', { lineHeight: '1.5' }],
'lg': ['1.25rem', { lineHeight: '1.4' }],
'xl': ['1.563rem', { lineHeight: '1.3' }],
'2xl': ['1.953rem', { lineHeight: '1.2' }],
'3xl': ['2.441rem', { lineHeight: '1.2' }],
'4xl': ['3.052rem', { lineHeight: '1.1' }],
},
extend: {
// Utilidades de tipografía fluida
fontSize: {
'fluid-sm': 'clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem)',
'fluid-base': 'clamp(1rem, 0.9rem + 0.5vw, 1.125rem)',
'fluid-lg': 'clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem)',
'fluid-xl': 'clamp(1.5rem, 1.25rem + 1.25vw, 2rem)',
'fluid-2xl': 'clamp(2rem, 1.5rem + 2.5vw, 3rem)',
'fluid-3xl': 'clamp(2.5rem, 2rem + 2.5vw, 4rem)',
}
}
}
}
Pruebas Automatizadas
Tests de Playwright para Redimensionamiento de Texto
import { test, expect } from '@playwright/test';
test.describe('Accesibilidad de Redimensionamiento de Texto', () => {
test('el contenido permanece visible al 200% de zoom', async ({ page }) => {
await page.goto('/');
// Obtener viewport inicial
const viewportSize = page.viewportSize();
// Simular 200% de zoom reduciendo viewport a la mitad
await page.setViewportSize({
width: viewportSize.width / 2,
height: viewportSize.height / 2
});
// Verificar que el contenido clave sigue visible
const heading = page.locator('h1').first();
await expect(heading).toBeVisible();
// Verificar que no se necesita barra de desplazamiento horizontal para contenido principal
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
// Algo de desplazamiento horizontal puede ser aceptable, pero excesivo no
const mainContent = page.locator('main');
const mainBox = await mainContent.boundingBox();
expect(mainBox.width).toBeLessThanOrEqual(viewportSize.width / 2 + 20);
});
test('el texto no está cortado en contenedores', async ({ page }) => {
await page.goto('/');
// Zoom al 200%
await page.setViewportSize({
width: 640,
height: 480
});
// Encontrar todos los contenedores de texto
const textContainers = page.locator('p, h1, h2, h3, h4, h5, h6, li, td, th');
const count = await textContainers.count();
for (let i = 0; i < Math.min(count, 20); i++) {
const container = textContainers.nth(i);
const isVisible = await container.isVisible();
if (isVisible) {
// Verificar que overflow no está oculto con recorte de contenido
const hasOverflowHidden = await container.evaluate(el => {
const style = window.getComputedStyle(el);
const parent = el.parentElement;
const parentStyle = parent ? window.getComputedStyle(parent) : null;
// Verificar si el contenido está recortado
return (style.overflow === 'hidden' || parentStyle?.overflow === 'hidden') &&
(el.scrollHeight > el.clientHeight);
});
expect(hasOverflowHidden).toBe(false);
}
}
});
test('los tamaños de fuente usan unidades relativas', async ({ page }) => {
await page.goto('/');
// Verificar que body y texto principal usan unidades relativas
const textElements = page.locator('body, p, h1, h2, h3, h4, h5, h6');
const count = await textElements.count();
for (let i = 0; i < count; i++) {
const element = textElements.nth(i);
// El estilo en línea no debe tener font-size en px fijos
const inlineStyle = await element.getAttribute('style');
if (inlineStyle) {
expect(inlineStyle).not.toMatch(/font-size:\s*\d+px/);
}
}
});
test('los elementos interactivos permanecen usables con zoom', async ({ page }) => {
await page.goto('/');
// Zoom al 200%
await page.setViewportSize({ width: 640, height: 480 });
// Verificar que los botones siguen visibles y clicables
const buttons = page.locator('button, [role="button"], a');
const count = await buttons.count();
for (let i = 0; i < Math.min(count, 10); i++) {
const button = buttons.nth(i);
const isVisible = await button.isVisible();
if (isVisible) {
const box = await button.boundingBox();
expect(box).not.toBeNull();
expect(box.width).toBeGreaterThan(0);
expect(box.height).toBeGreaterThan(0);
}
}
});
});
Checklist de Resumen
- [ ] Todos los tamaños de fuente usan unidades rem o em
- [ ] El tamaño de fuente raíz respeta preferencias del usuario (usar % o nada)
- [ ] Las alturas de línea son sin unidades para escalado apropiado
- [ ] Los contenedores usan min-height en lugar de altura fija
- [ ] Los layouts usan anchos flexibles (%, flex, grid)
- [ ] No hay overflow:hidden en contenedores de texto
- [ ] El contenido permanece visible al 200% de zoom
- [ ] No se requiere desplazamiento horizontal para contenido principal
- [ ] Los elementos interactivos permanecen accesibles con zoom
- [ ] Probado con zoom del navegador y zoom solo de texto
Artículos Relacionados
- Guía de Contraste de Color - Accesibilidad visual
- Hub de Cumplimiento WCAG - Todos los evaluadores de accesibilidad