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
- W3C - WCAG 2.2 SC 1.4.5 Images of Text
- W3C - C22 Using CSS to control visual presentation of text
- W3C - G140 Separating information and structure from presentation
- MDN - CSS Text
- Google Fonts - Using web fonts