Documentación de UXR SEO Analyzer

Introducción

Redimensionar Texto

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

  1. Abre tu sitio web en un navegador
  2. Usa Ctrl/Cmd + para hacer zoom al 200%
  3. Verifica que todo el texto sea legible
  4. Comprueba que ningún contenido esté cortado u oculto
  5. Asegura que todos los elementos interactivos sigan siendo usables
  6. 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


Referencias

  1. W3C - WCAG 2.2 SC 1.4.4 Resize Text
  2. W3C - C28 Specifying the size of text containers using em units
  3. W3C - G179 Ensuring no loss of content when scaling
  4. WebAIM - Accessible CSS
  5. W3C - C12 Using percent for font sizes
  6. W3C - C14 Using em units for font sizes
  7. MDN - CSS values and units

Artículos relacionados

Hub de categoría

Hub

Hub de Cumplimiento WCAG 2.2

La accesibilidad web asegura que los sitios web y aplicaciones puedan ser utilizados por todos, incluyendo personas con discapacidades

Última actualización: