Detailed guide

Images Of Text Implementation Guide

View contents

Images of Text Implementation Guide: CSS Techniques for Accessible Typography

Introduction

Modern CSS provides powerful tools to achieve virtually any typographic effect without resorting to images of text. This guide covers implementation patterns for custom fonts, text effects, and decorative typography that maintain accessibility while achieving sophisticated visual designs.

Using CSS for text presentation ensures users can customize their experience while search engines and assistive technologies can properly interpret content.

Web Fonts Implementation

Google Fonts Integration

<!-- In <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">
/* Usage */
.decorative-heading {
  font-family: 'Playfair Display', Georgia, serif;
  font-weight: 700;
  font-size: 3rem;
}

Self-Hosted Fonts

/* Font-face declaration for custom fonts */
@font-face {
  font-family: 'BrandFont';
  src: url('/fonts/brandfont.woff2') format('woff2'),
       url('/fonts/brandfont.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* Prevents invisible text during load */
}

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

/* Variable font (single file, multiple weights) */
@font-face {
  font-family: 'BrandFontVariable';
  src: url('/fonts/brandfont-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

CSS Text Effects

Gradient Text

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

/* Animated gradient */
.animated-gradient-text {
  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: gradient-shift 8s ease infinite;
}

@keyframes gradient-shift {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

Text Shadows

/* Subtle shadow for depth */
.subtle-shadow {
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Bold shadow */
.bold-shadow {
  text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
}

/* Glowing text */
.glow-text {
  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);
}

/* Neon effect */
.neon-text {
  color: #fff;
  text-shadow:
    0 0 5px #fff,
    0 0 10px #fff,
    0 0 20px #0ff,
    0 0 30px #0ff,
    0 0 40px #0ff;
}

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

Text Stroke/Outline

/* Outlined text */
.outlined-text {
  -webkit-text-stroke: 2px #333;
  color: transparent;
}

/* Filled with stroke */
.stroked-filled {
  -webkit-text-stroke: 1px #000;
  color: #fff;
}

/* Double outline effect */
.double-outline {
  color: #fff;
  -webkit-text-stroke: 3px #000;
  paint-order: stroke fill;
}

Letter Spacing and Transforms

/* Spaced uppercase (good for headings) */
.spaced-caps {
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-weight: 600;
}

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

/* Condensed tracking */
.tight-tracking {
  letter-spacing: -0.02em;
}

Decorative Typography Components

React Component

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>
  );
}

// Usage
<DecorativeText variant="gradient" as="h1" className="text-5xl font-bold">
  Beautiful Heading
</DecorativeText>

Vue 3 Component

<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>

Converting Images of Text to CSS

Before: Image Banner

<!-- OLD: Image with text baked in -->
<img src="sale-banner.png" alt="Summer Sale - Up to 50% Off - Shop Now">

After: CSS Banner

<section class="sale-banner">
  <div class="banner-content">
    <p class="banner-eyebrow">Limited Time</p>
    <h2 class="banner-heading">Summer Sale</h2>
    <p class="banner-discount">Up to <span class="highlight">50% Off</span></p>
    <a href="/sale" class="banner-cta">Shop Now</a>
  </div>
</section>
.sale-banner {
  background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%);
  padding: 3rem;
  text-align: center;
  border-radius: 1rem;
}

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

.banner-heading {
  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-discount {
  font-size: 1.5rem;
  color: #fff;
}

.highlight {
  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);
}

SVG Text for Complex Effects

<!-- SVG text with effects not possible in CSS -->
<svg viewBox="0 0 400 100" class="svg-heading" aria-labelledby="svg-title">
  <title id="svg-title">Creative Typography</title>
  <defs>
    <linearGradient id="text-gradient" 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="shadow">
      <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(#text-gradient)"
    filter="url(#shadow)"
    font-family="Georgia, serif"
    font-size="48"
    font-weight="bold"
  >
    Creative Typography
  </text>
</svg>
.svg-heading {
  width: 100%;
  max-width: 400px;
  height: auto;
}

/* SVG text inherits font from CSS if font not specified */
.svg-heading text {
  font-family: inherit;
}

Automated Testing

Playwright Tests for Images of Text

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

test.describe('Images of Text Compliance', () => {
  test('no images contain text except logos', async ({ page }) => {
    await page.goto('/');

    // Get all images
    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') || '';

      // Skip logo images (acceptable)
      const isLogo = className.includes('logo') ||
                     src.includes('logo') ||
                     alt.toLowerCase().includes('logo');

      if (!isLogo) {
        // Check if alt text suggests this is text content
        const hasTextContent = /^[A-Z]/.test(alt) && // Starts with capital
                               alt.length > 20 && // Long enough to be a sentence
                               !alt.includes('photo') &&
                               !alt.includes('image') &&
                               !alt.includes('illustration');

        if (hasTextContent) {
          console.warn(`Potential image of text: ${src} - Alt: "${alt}"`);
        }
      }
    }
  });

  test('headings are real text elements', async ({ page }) => {
    await page.goto('/');

    // All h1-h6 should be actual text, not images
    const headings = page.locator('h1, h2, h3, h4, h5, h6');
    const count = await headings.count();

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

      // Should not contain img tags
      expect(innerHTML).not.toMatch(/<img/i);

      // Should have actual text content
      const textContent = await heading.textContent();
      expect(textContent?.trim().length).toBeGreaterThan(0);
    }
  });

  test('buttons contain text not images', async ({ page }) => {
    await page.goto('/');

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

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

      // Allow SVG icons, but not img tags for text
      const hasImgTag = /<img[^>]*alt=["'][^"']{10,}["']/i.test(innerHTML);

      if (hasImgTag) {
        console.warn('Button may contain image of text:', innerHTML);
      }

      // Should have accessible name (text or aria-label)
      const textContent = await button.textContent();
      const ariaLabel = await button.getAttribute('aria-label');

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

  test('decorative text uses CSS not images', async ({ page }) => {
    await page.goto('/');

    // Check for elements that might be styled text
    const styledElements = page.locator('.gradient-text, .decorative, .fancy-text, [class*="text-effect"]');
    const count = await styledElements.count();

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

      // Should not be img
      expect(tagName).not.toBe('IMG');
    }
  });
});

Summary Checklist

  • [ ] All text content uses real HTML text elements
  • [ ] Custom fonts use @font-face or web font services
  • [ ] Text effects achieved with CSS (gradients, shadows, outlines)
  • [ ] Banners and CTAs use HTML/CSS, not images
  • [ ] Only logos contain essential images of text
  • [ ] SVG text includes proper title/aria-label
  • [ ] Text scales properly when zoomed
  • [ ] Text can be selected and copied
  • [ ] Screen readers read text naturally

References

  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

Related articles

Related version

Introduction

Images Of Text Explained

WCAG 1.4.5 (Images of Text) requires that if the technologies being used can achieve the visual presentation, text is used to convey information rather than...

Category hub

Hub

Wcag Compliance Hub

Web accessibility ensures that websites and applications can be used by everyone, including people with disabilities