Guía detallada

Images Of Text Implementation Guide

Ver contenido

Guía de Implementación de Imágenes de Texto: Técnicas CSS para Tipografía Accesible

Introducción

CSS moderno proporciona herramientas poderosas para lograr virtualmente cualquier efecto tipográfico sin recurrir a imágenes de texto. Esta guía cubre patrones de implementación para fuentes personalizadas, efectos de texto y tipografía decorativa que mantienen la accesibilidad mientras logran diseños visuales sofisticados.

Usar CSS para la presentación de texto asegura que los usuarios puedan personalizar su experiencia mientras los motores de búsqueda y tecnologías de asistencia pueden interpretar correctamente el contenido.

Implementación de Web Fonts

Integración de Google Fonts

<!-- En <head> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap" rel="stylesheet">
/* Uso */
.encabezado-decorativo {
  font-family: 'Playfair Display', Georgia, serif;
  font-weight: 700;
  font-size: 3rem;
}

Fuentes Auto-Alojadas

/* Declaración font-face para fuentes personalizadas */
@font-face {
  font-family: 'FuenteMarca';
  src: url('/fonts/fuentemarca.woff2') format('woff2'),
       url('/fonts/fuentemarca.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* Previene texto invisible durante carga */
}

@font-face {
  font-family: 'FuenteMarca';
  src: url('/fonts/fuentemarca-bold.woff2') format('woff2'),
       url('/fonts/fuentemarca-bold.woff') format('woff');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Fuente variable (archivo único, múltiples pesos) */
@font-face {
  font-family: 'FuenteMarcaVariable';
  src: url('/fonts/fuentemarca-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

Efectos de Texto CSS

Texto con Gradiente

/* Relleno gradiente en texto */
.texto-gradiente {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent; /* Fallback */
}

/* Gradiente animado */
.texto-gradiente-animado {
  background: linear-gradient(
    90deg,
    #ff6b6b,
    #feca57,
    #48dbfb,
    #ff9ff3,
    #ff6b6b
  );
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: desplazar-gradiente 8s ease infinite;
}

@keyframes desplazar-gradiente {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

Sombras de Texto

/* Sombra sutil para profundidad */
.sombra-sutil {
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Sombra pronunciada */
.sombra-fuerte {
  text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
}

/* Texto brillante */
.texto-brillo {
  text-shadow:
    0 0 10px rgba(255, 255, 255, 0.8),
    0 0 20px rgba(255, 255, 255, 0.6),
    0 0 30px rgba(255, 255, 255, 0.4);
}

/* Efecto neón */
.texto-neon {
  color: #fff;
  text-shadow:
    0 0 5px #fff,
    0 0 10px #fff,
    0 0 20px #0ff,
    0 0 30px #0ff,
    0 0 40px #0ff;
}

/* Efecto 3D retro */
.retro-3d {
  color: #f5f5f5;
  text-shadow:
    1px 1px 0 #c4c4c4,
    2px 2px 0 #b4b4b4,
    3px 3px 0 #a4a4a4,
    4px 4px 0 #949494;
}

Trazo/Contorno de Texto

/* Texto con contorno */
.texto-contorno {
  -webkit-text-stroke: 2px #333;
  color: transparent;
}

/* Relleno con trazo */
.trazo-relleno {
  -webkit-text-stroke: 1px #000;
  color: #fff;
}

/* Efecto doble contorno */
.doble-contorno {
  color: #fff;
  -webkit-text-stroke: 3px #000;
  paint-order: stroke fill;
}

Espaciado de Letras y Transformaciones

/* Mayúsculas espaciadas (ideal para encabezados) */
.mayusculas-espaciadas {
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-weight: 600;
}

/* Versalitas */
.versalitas {
  font-variant: small-caps;
  letter-spacing: 0.05em;
}

/* Espaciado condensado */
.espaciado-ajustado {
  letter-spacing: -0.02em;
}

Componentes de Tipografía Decorativa

Componente React

import React from 'react';

interface DecorativeTextProps {
  children: React.ReactNode;
  variant: 'gradient' | 'shadow' | 'outline' | 'glow';
  as?: 'h1' | 'h2' | 'h3' | 'h4' | 'span' | 'p';
  className?: string;
}

const variantStyles = {
  gradient: `
    bg-gradient-to-r from-purple-600 to-pink-600
    bg-clip-text text-transparent
  `,
  shadow: `
    text-gray-900
    [text-shadow:_3px_3px_0_rgb(0_0_0_/_20%)]
  `,
  outline: `
    text-transparent
    [-webkit-text-stroke:_2px_currentColor]
    text-gray-900
  `,
  glow: `
    text-white
    [text-shadow:_0_0_10px_rgba(255,255,255,0.8),_0_0_20px_rgba(255,255,255,0.6)]
  `
};

export function DecorativeText({
  children,
  variant,
  as: Component = 'span',
  className = ''
}: DecorativeTextProps) {
  return (
    <Component className={`${variantStyles[variant]} ${className}`}>
      {children}
    </Component>
  );
}

// Uso
<DecorativeText variant="gradient" as="h1" className="text-5xl font-bold">
  Encabezado Hermoso
</DecorativeText>

Componente Vue 3

<script setup lang="ts">
import { computed } from 'vue'

type TextVariant = 'gradient' | 'shadow' | 'outline' | 'glow'

interface Props {
  variant: TextVariant
  as?: string
}

const props = withDefaults(defineProps<Props>(), {
  as: 'span'
})

const variantClasses = computed(() => {
  const variants: Record<TextVariant, string> = {
    gradient: 'decorative-gradient',
    shadow: 'decorative-shadow',
    outline: 'decorative-outline',
    glow: 'decorative-glow'
  }
  return variants[props.variant]
})
</script>

<template>
  <component :is="as" :class="variantClasses">
    <slot />
  </component>
</template>

<style scoped>
.decorative-gradient {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.decorative-shadow {
  text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
}

.decorative-outline {
  -webkit-text-stroke: 2px currentColor;
  color: transparent;
}

.decorative-glow {
  color: #fff;
  text-shadow:
    0 0 10px rgba(255, 255, 255, 0.8),
    0 0 20px rgba(255, 255, 255, 0.6);
}
</style>

Convirtiendo Imágenes de Texto a CSS

Antes: Banner de Imagen

<!-- ANTIGUO: Imagen con texto incrustado -->
<img src="banner-oferta.png" alt="Oferta de Verano - Hasta 50% de Descuento - Compra Ahora">

Después: Banner CSS

<section class="banner-oferta">
  <div class="banner-contenido">
    <p class="banner-subtitulo">Tiempo Limitado</p>
    <h2 class="banner-titulo">Oferta de Verano</h2>
    <p class="banner-descuento">Hasta <span class="resaltado">50% de Descuento</span></p>
    <a href="/ofertas" class="banner-cta">Compra Ahora</a>
  </div>
</section>
.banner-oferta {
  background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%);
  padding: 3rem;
  text-align: center;
  border-radius: 1rem;
}

.banner-subtitulo {
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-size: 0.875rem;
  color: rgba(255, 255, 255, 0.9);
  margin-bottom: 0.5rem;
}

.banner-titulo {
  font-family: 'Playfair Display', serif;
  font-size: clamp(2rem, 5vw, 4rem);
  color: #fff;
  margin-bottom: 0.5rem;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}

.banner-descuento {
  font-size: 1.5rem;
  color: #fff;
}

.resaltado {
  font-weight: 700;
  font-size: 2rem;
}

.banner-cta {
  display: inline-block;
  margin-top: 1.5rem;
  padding: 1rem 2rem;
  background: #fff;
  color: #ff6b6b;
  font-weight: 600;
  border-radius: 2rem;
  text-decoration: none;
  transition: transform 0.2s, box-shadow 0.2s;
}

.banner-cta:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

Texto SVG para Efectos Complejos

<!-- Texto SVG con efectos no posibles en CSS -->
<svg viewBox="0 0 400 100" class="encabezado-svg" aria-labelledby="titulo-svg">
  <title id="titulo-svg">Tipografía Creativa</title>
  <defs>
    <linearGradient id="gradiente-texto" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" style="stop-color:#667eea"/>
      <stop offset="100%" style="stop-color:#764ba2"/>
    </linearGradient>
    <filter id="sombra">
      <feDropShadow dx="2" dy="2" stdDeviation="2" flood-opacity="0.3"/>
    </filter>
  </defs>
  <text
    x="50%"
    y="50%"
    dominant-baseline="middle"
    text-anchor="middle"
    fill="url(#gradiente-texto)"
    filter="url(#sombra)"
    font-family="Georgia, serif"
    font-size="48"
    font-weight="bold"
  >
    Tipografía Creativa
  </text>
</svg>
.encabezado-svg {
  width: 100%;
  max-width: 400px;
  height: auto;
}

/* El texto SVG hereda la fuente de CSS si no se especifica */
.encabezado-svg text {
  font-family: inherit;
}

Pruebas Automatizadas

Pruebas Playwright para Imágenes de Texto

import { test, expect } from '@playwright/test';

test.describe('Cumplimiento de Imágenes de Texto', () => {
  test('ninguna imagen contiene texto excepto logos', async ({ page }) => {
    await page.goto('/');

    // Obtener todas las imágenes
    const images = page.locator('img');
    const count = await images.count();

    for (let i = 0; i < count; i++) {
      const img = images.nth(i);
      const alt = await img.getAttribute('alt') || '';
      const src = await img.getAttribute('src') || '';
      const className = await img.getAttribute('class') || '';

      // Omitir imágenes de logo (aceptables)
      const esLogo = className.includes('logo') ||
                     src.includes('logo') ||
                     alt.toLowerCase().includes('logo');

      if (!esLogo) {
        // Verificar si el texto alt sugiere que es contenido de texto
        const tieneContenidoTexto = /^[A-Z]/.test(alt) && // Comienza con mayúscula
                                    alt.length > 20 && // Suficientemente largo para ser una oración
                                    !alt.includes('foto') &&
                                    !alt.includes('imagen') &&
                                    !alt.includes('ilustración');

        if (tieneContenidoTexto) {
          console.warn(`Posible imagen de texto: ${src} - Alt: "${alt}"`);
        }
      }
    }
  });

  test('los encabezados son elementos de texto reales', async ({ page }) => {
    await page.goto('/');

    // Todos los h1-h6 deben ser texto real, no imágenes
    const encabezados = page.locator('h1, h2, h3, h4, h5, h6');
    const count = await encabezados.count();

    for (let i = 0; i < count; i++) {
      const encabezado = encabezados.nth(i);
      const innerHTML = await encabezado.innerHTML();

      // No debe contener etiquetas img
      expect(innerHTML).not.toMatch(/<img/i);

      // Debe tener contenido de texto real
      const textContent = await encabezado.textContent();
      expect(textContent?.trim().length).toBeGreaterThan(0);
    }
  });

  test('los botones contienen texto no imágenes', async ({ page }) => {
    await page.goto('/');

    const botones = page.locator('button, [role="button"], .btn');
    const count = await botones.count();

    for (let i = 0; i < count; i++) {
      const boton = botones.nth(i);
      const innerHTML = await boton.innerHTML();

      // Permitir iconos SVG, pero no etiquetas img para texto
      const tieneEtiquetaImg = /<img[^>]*alt=["'][^"']{10,}["']/i.test(innerHTML);

      if (tieneEtiquetaImg) {
        console.warn('El botón puede contener imagen de texto:', innerHTML);
      }

      // Debe tener nombre accesible (texto o aria-label)
      const textContent = await boton.textContent();
      const ariaLabel = await boton.getAttribute('aria-label');

      expect(textContent?.trim() || ariaLabel).toBeTruthy();
    }
  });

  test('el texto decorativo usa CSS no imágenes', async ({ page }) => {
    await page.goto('/');

    // Verificar elementos que podrían ser texto estilizado
    const elementosEstilizados = page.locator('.texto-gradiente, .decorativo, .texto-elegante, [class*="efecto-texto"]');
    const count = await elementosEstilizados.count();

    for (let i = 0; i < count; i++) {
      const elemento = elementosEstilizados.nth(i);
      const tagName = await elemento.evaluate(el => el.tagName);

      // No debe ser img
      expect(tagName).not.toBe('IMG');
    }
  });
});

Lista de Verificación Resumen

  • [ ] Todo el contenido de texto usa elementos HTML de texto real
  • [ ] Las fuentes personalizadas usan @font-face o servicios de web fonts
  • [ ] Los efectos de texto se logran con CSS (gradientes, sombras, contornos)
  • [ ] Los banners y CTAs usan HTML/CSS, no imágenes
  • [ ] Solo los logos contienen imágenes de texto esenciales
  • [ ] El texto SVG incluye title/aria-label apropiados
  • [ ] El texto escala correctamente al hacer zoom
  • [ ] El texto se puede seleccionar y copiar
  • [ ] Los lectores de pantalla leen el texto naturalmente

Referencias

  1. W3C - WCAG 2.2 SC 1.4.5 Images of Text
  2. W3C - C22 Using CSS to control visual presentation of text
  3. W3C - G140 Separating information and structure from presentation
  4. MDN - CSS Text
  5. Google Fonts - Using web fonts

Artículos relacionados

Versión relacionada

Introducción

Images Of Text Explained

WCAG 1.4.5 (Imágenes de Texto) requiere que si las tecnologías utilizadas pueden lograr la presentación visual, se use texto para transmitir información en...

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