Guía detallada

Resize Text Implementation Guide

Ver contenido

Guía de Implementación de Redimensionar Texto: Patrones Completos para Tipografía Escalable

Introducción

Crear tipografía escalable que funcione al 200% de zoom requiere atención cuidadosa a las unidades CSS, dimensionamiento de contenedores y flexibilidad del diseño. Esta guía cubre patrones completos de implementación para texto amigable con el redimensionamiento que cumple con los requisitos de WCAG 1.4.4 mientras mantiene calidad visual a cualquier tamaño.

Más allá del cumplimiento, la tipografía escalable bien implementada mejora la experiencia de usuario en todos los dispositivos y acomoda preferencias diversas de los usuarios.

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

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 - C12 Using percent for font sizes
  4. W3C - C14 Using em units for font sizes
  5. MDN - CSS values and units

Artículos relacionados

Versión relacionada

Introducción

Resize Text Explained

Muchos usuarios con baja visión necesitan agrandar el texto para leer cómodamente

Hub de categoría

Hub

Wcag Compliance Hub

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