---
name: outline-text-hover
description: Use when you want "Editorial, creative, clean" - Switches between filled and outlined typography on hover.
---

# Outline Text Hover

> **Category:** Text  -  **Personality:** Editorial, creative, clean
>
> **Best use:** Navigation, large headings
>
> **Ratings:** Professional 4/5 - Casual 3/5 - Exotic 3/5 - Visual impact 3/5 - Difficulty 2/5 - Performance cost 1/5

## What it is

Large type that sits **outlined** at rest — only a thin stroke, no fill — and **fills in solid on hover or keyboard focus**, with the fill wiping across the letters rather than snapping. The outline is `-webkit-text-stroke` plus a transparent text fill; the wipe is a solid-colour gradient clipped to the glyphs (`background-clip: text`) whose width animates from `0%` to `100%`. It reads as confident and editorial, which is why it suits big nav lists, magazine-style indexes and oversized headings where each line should feel like a deliberate, tactile choice.

## Dependencies / CDN

None for the effect — pure CSS, with an optional sprinkle of vanilla JS for the directional wipe. The demo only loads display/body fonts (optional):

```html
<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=Anton&family=Schibsted+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
```

## HTML

A nav list where each row is one big word (plus optional editorial meta). The word is its own element so the stroke/fill apply only to it:

```html
<nav class="ot-list">
  <a class="ot-row" href="#work">
    <span class="ot-lead"><span class="ot-num">01</span><span class="ot-word">Work</span></span>
    <span class="ot-meta"><span class="ot-desc">Selected projects</span><span class="ot-arrow" aria-hidden="true">&rarr;</span></span>
  </a>
  <!-- ...more rows... -->
</nav>
```

## CSS

```css
/* THE EFFECT — outline at rest, gradient-fill wiped in on hover/focus */
.ot-word{
  display:inline-block;                 /* gives the wipe a box to grow in */
  font-family:'Anton',sans-serif;
  font-size:clamp(38px,9.4vw,104px); text-transform:uppercase; line-height:.94;

  /* 1) the OUTLINE: transparent fill + a stroke */
  color:transparent;
  -webkit-text-fill-color:transparent;
  -webkit-text-stroke:2px #171310;

  /* 2) the FILL: a solid gradient clipped to the glyphs, zero-width at rest */
  background-image:linear-gradient(#171310,#171310);
  background-repeat:no-repeat;
  background-position:var(--ot-x,0%) 0;  /* anchor side (JS may flip to 100%) */
  background-size:0% 100%;               /* 0% width = nothing shows = outline */
  -webkit-background-clip:text;
          background-clip:text;

  transition:background-size .55s cubic-bezier(.76,0,.24,1);  /* the wipe */
}
/* hover AND keyboard focus both fill — keep parity */
.ot-row:hover .ot-word,
.ot-row:focus-visible .ot-word{ background-size:100% 100%; }

/* optional: an accent underline that draws in from the same side */
.ot-word{ position:relative; }
.ot-word::after{
  content:""; position:absolute; left:0; right:0; bottom:-.02em; height:3px;
  background:#d8401c; transform:scaleX(0); transform-origin:var(--ot-x,0%) 50%;
  transition:transform .5s cubic-bezier(.76,0,.24,1);
}
.ot-row:hover .ot-word::after,
.ot-row:focus-visible .ot-word::after{ transform:scaleX(1); }

/* fallback where background-clip:text is unsupported → just show filled words */
@supports not ((-webkit-background-clip:text) or (background-clip:text)){
  .ot-word{ color:#171310; -webkit-text-fill-color:#171310; background:none; }
}

/* a clear keyboard focus ring (don't rely on the fill alone) */
.ot-row:focus{ outline:none; }
.ot-row:focus-visible{ outline:2.5px solid #d8401c; outline-offset:8px; border-radius:4px; }
```

## JavaScript

Optional. The hover/focus fill is pure CSS; this only makes the wipe start from the side the pointer enters (and drain toward the side it leaves) by flipping the `--ot-x` anchor. No loops, so no `prefers-reduced-motion` guard is needed — the media query below already neutralises the transition.

```js
document.querySelectorAll('.ot-row').forEach(function(row){
  function side(e){
    var r = row.getBoundingClientRect();
    return (e.clientX - r.left) < r.width / 2 ? '0%' : '100%';
  }
  row.addEventListener('mouseenter', function(e){ row.style.setProperty('--ot-x', side(e)); });
  row.addEventListener('mouseleave', function(e){ row.style.setProperty('--ot-x', side(e)); });
  row.addEventListener('focus',      function(){ row.style.setProperty('--ot-x', '0%'); }); // keyboard = left-to-right
});
```

## How it works

- **Outline** = `-webkit-text-fill-color: transparent` (the glyph interior is empty) + `-webkit-text-stroke` (a stroke drawn along the glyph path). Together they give hollow, outlined letters.
- **Fill** = a `linear-gradient` of one solid colour used as the element's background, then `background-clip: text` clips that background to the glyph shapes. Because the text fill is transparent, the clipped background shows *through* as the letter fill.
- **The wipe** = animating `background-size` from `0% 100%` to `100% 100%`. At `0%` width the gradient paints nothing, so only the stroke is visible; growing the width reveals the fill progressively. `background-position` decides which edge it grows from.
- `transform-origin` on the underline reads the same `--ot-x` variable, so the accent rule sweeps in the same direction as the fill.

## Customization

- **Direction:** set `background-position` / `--ot-x` to `0%` (L→R), `100%` (R→L), or use `0% 100%` + `background-size:100% 0%` for a bottom-up wipe.
- **Invert it:** start `background-size:100% 100%` (filled) and shrink to `0%` on hover for a "hollow on hover" variant.
- **Two-tone:** make the stroke and the gradient *different* colours for an outlined-fill look, or use a multi-stop gradient for a colour sweep.
- **Stroke weight:** scale `-webkit-text-stroke-width` with viewport (the demo bumps it at `min-width:780px`). Heavier strokes read better at large sizes.
- **Speed/feel:** tune the `transition` duration and easing; `cubic-bezier(.76,0,.24,1)` gives a crisp editorial snap.

## Accessibility & performance

- **Keyboard parity:** every `:hover` rule is duplicated for `:focus-visible`, and a visible focus ring is added so the fill isn't the *only* affordance.
- **Reduced motion:** wrap the move/wipe so it becomes instant — `@media (prefers-reduced-motion: reduce){ .ot-word{transition:none} }` (the catalog page also zeroes transition durations globally). State still changes, just without animation.
- **Contrast:** outlined text has lower contrast than filled text. Keep the stroke thick enough and the type large enough to stay legible at rest; don't use this for small or body-sized copy.
- **Cheap:** animating `background-size` (and `transform` on the underline) is GPU-friendly; there is no layout thrash and no per-frame JS.

## Gotchas

- **`background-clip: text` needs the `-webkit-` prefix** (and `-webkit-text-fill-color: transparent`) or the gradient fills the whole box instead of the letters. Always ship the `@supports not (...)` fallback so unsupported browsers get plain filled words.
- **The element must be `display:inline-block`** (or block) — the `background-size` wipe has nothing to grow inside a plain inline element.
- **Sub-pixel stroke seams:** with a tight `line-height`, very large glyphs can poke outside the background box and leave a hair of unfilled stroke. Keep `line-height` around `.94–1` for all-caps, or pad the element.
- **Don't animate `-webkit-text-stroke-width`** to "fill" — it's not smoothly animatable and looks chunky. The gradient-clip wipe is the smooth path.
- Setting only the text fill transparent (no stroke, no clipped background) gives *invisible* text — all three pieces (transparent fill + stroke + clipped background) are required.
