Guía detallada

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

Ver contenido

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

Redimensionar Texto: Asegurando que el Contenido Permanezca Usable al 200% de Zoom

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

Hub de categoría

Hub

Hub de Cumplimiento WCAG 2.2: Guía Completa de Accesibilidad Web

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