Character-Level Styling: CSS Tricks and JavaScript Fallbacks

Targeting individual characters with CSS usually demands manual HTML changes, like wrapping each character in a span. While CSS has no general-purpose selector for this, a handful of niche techniques can handle common cases. When those fall short, JavaScript—either at runtime or during a build step—offers a robust alternative.

Character-Specific CSS Techniques

Custom Glyphs via unicode-range

The unicode-range descriptor of the @font-face rule lets a custom font apply only to selected characters. For instance, if your headings feature ampersands that deserve a more decorative look, you can load a special font just for the ampersand (U+0026) while the surrounding text uses the standard heading typeface:

@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300');

h1, h2, h3, h4, h5, h6 {
  font-family:  'Ampersand', Montserrat, sans-serif;
}

@font-face {
  font-family: 'Ampersand';
  src: local('Times New Roman');
  unicode-range: U+0026;
}

Apply it to this markup:

<h1>Jane Austen Novels</h1>
<h2>Pride & Prejudice</h2>
<h2>Sense & Sensibility</h2>

Styling the First or Last Character

The ::first-letter pseudo-element is widely supported and was built for drop caps, but it works fine for any single-character styling at the start of a block:

p::first-letter {
  font-size: 125%;
  font-weight: bold;
}

Broader pseudo-elements like ::nth-letter have been proposed, but they aren’t implemented. For the concluding character, the ::after pseudo-element with a content value can add a fixed character without touching HTML. This adds an italicized exclamation mark after every h2:

h2::after {
  content: '\0021';
  color: red;
  font-style: italic;
}

Alternate Glyphs (font-variant-alternates)

For fonts that ship alternate forms of a letter, the font-variant-alternates property with the character-variant() function selects a preferred glyph. This is only available in Firefox and isn’t production-ready, but it’s worth knowing for niche, self-contained projects.

JavaScript Approaches for Precise Replacements

Runtime Find-and-Replace

Consider a header where the word “LOGO” appears, and only the first “O” needs custom styling. A client-side script can scan headings and wrap that letter in a .special-o span:

const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6");

for (const heading of headings) {
  heading.innerHTML = heading.innerHTML
    .replace(/\bLOGO\b/g, 'L<span class="special-o">O</span>GO');
}

The regular expression’s word boundary (\b) prevents matches inside longer words like “LOGOS.” This level of specificity isn’t reachable with pure CSS, and the same function can swap the letter for an image if needed.

Build-Time Replacement with webpack

Runtime DOM modifications cost little, but moving the work into a build step eliminates client overhead entirely. With webpack, the string-replace-loader plugin performs the replacement before the code ships. Install it first:

npm install --save-dev string-replace-loader

Then wire it into webpack.config.js:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.html$/i,
        loader: 'string-replace-loader',
        options: {
          search: '/\bLOGO\b/g',
          replace: 'L<span class="special-o">O</span>GO',
        }
      }
    ]
  }
}

Adjust the test property to apply the loader to your templating format, be it JSX, TSX, PUG, Handlebars, or another:

/\.html$/i # HTML
/\.[jt]sx$/i # JSX or TSX
/\.pug$/i # PUG
/\.handlebars$/i # Handlebars

Because the transformation happens at build time, the browser downloads no JavaScript for this task. If your needs are purely visual and you’re willing to edit fonts, building a custom typeface with free tools like Font Forge or Birdfont sidesteps both CSS limitations and script injection.