39 CSS Breadcrumbs21 / 39
Editorial Blog Breadcrumb with Animated Wave Underline Hover
Editorial publishing breadcrumb in the spirit of Medium, Substack, and NYT — warm cream paper, muted ink serif type, and a signature hover flourish where an inline SVG wave underline draws itself beneath each crumb like a printer's flourish finishing on the page.
Published
The code
<section class="bc-21" aria-label="Editorial blog breadcrumb with wave underline demo">
<article class="bc-21__frame">
<nav aria-label="Breadcrumb" class="bc-21__nav">
<ol class="bc-21__list">
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Home</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Style</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Fashion</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item" aria-current="page">
<span class="bc-21__link bc-21__link--current">
<span class="bc-21__label">The Return of Tailored Denim</span>
</span>
</li>
</ol>
</nav>
<h1 class="bc-21__headline">The Return of Tailored Denim</h1>
<p class="bc-21__dek">A quiet revival of pressed seams and structured silhouettes, filed under Style / Fashion.</p>
</article>
</section><section class="bc-21" aria-label="Editorial blog breadcrumb with wave underline demo">
<article class="bc-21__frame">
<nav aria-label="Breadcrumb" class="bc-21__nav">
<ol class="bc-21__list">
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Home</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Style</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Fashion</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item" aria-current="page">
<span class="bc-21__link bc-21__link--current">
<span class="bc-21__label">The Return of Tailored Denim</span>
</span>
</li>
</ol>
</nav>
<h1 class="bc-21__headline">The Return of Tailored Denim</h1>
<p class="bc-21__dek">A quiet revival of pressed seams and structured silhouettes, filed under Style / Fashion.</p>
</article>
</section>.bc-21 {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(2rem, 6vw, 4rem) clamp(1rem, 4vw, 2.5rem);
background: oklch(0.97 0.01 85);
font-family: Charter, 'Iowan Old Style', 'Palatino Linotype', Georgia, 'Times New Roman', serif;
color: oklch(0.28 0.02 60);
-webkit-font-smoothing: antialiased;
--bc-21-ink: oklch(0.42 0.02 60);
--bc-21-ink-strong: oklch(0.22 0.02 60);
--bc-21-accent: oklch(0.55 0.14 30);
--bc-21-draw: 420ms;
--bc-21-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
.bc-21 *,
.bc-21 *::before,
.bc-21 *::after {
box-sizing: border-box;
}
.bc-21__frame {
width: 100%;
max-width: 62ch;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.bc-21__nav {
font-size: 0.9375rem;
line-height: 1.4;
}
.bc-21__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.15rem 0.35rem;
}
.bc-21__item {
display: inline-flex;
align-items: center;
min-height: 44px;
color: var(--bc-21-ink);
}
.bc-21__item + .bc-21__item::before {
content: "·";
margin: 0 0.55rem 0 0.2rem;
color: oklch(0.62 0.02 60);
font-size: 1.05em;
line-height: 1;
transform: translateY(-0.05em);
}
.bc-21__link {
position: relative;
display: inline-flex;
align-items: center;
padding: 0.6rem 0.15rem;
min-height: 44px;
color: var(--bc-21-ink);
text-decoration: none;
font-style: italic;
letter-spacing: 0.005em;
transition: color 220ms var(--bc-21-ease);
border-radius: 2px;
}
.bc-21__label {
position: relative;
display: inline-block;
}
.bc-21__wave {
position: absolute;
left: 0;
right: 0;
bottom: 0.35rem;
width: 100%;
height: 6px;
color: var(--bc-21-accent);
overflow: visible;
pointer-events: none;
}
.bc-21__wave path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
opacity: 0;
transition: stroke-dashoffset var(--bc-21-draw) var(--bc-21-ease), opacity 180ms var(--bc-21-ease);
}
.bc-21__link:hover,
.bc-21__link:focus-visible {
color: var(--bc-21-ink-strong);
}
.bc-21__link:hover .bc-21__wave path,
.bc-21__link:focus-visible .bc-21__wave path {
stroke-dashoffset: 0;
opacity: 1;
}
.bc-21__link:focus-visible {
outline: 2px solid var(--bc-21-accent);
outline-offset: 3px;
}
.bc-21__link--current {
color: var(--bc-21-ink-strong);
font-style: normal;
font-weight: 600;
cursor: default;
}
.bc-21__link--current .bc-21__wave {
display: none;
}
.bc-21__headline {
margin: 0.5rem 0 0;
font-size: clamp(1.75rem, 4vw, 2.4rem);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.01em;
color: var(--bc-21-ink-strong);
}
.bc-21__dek {
margin: 0;
font-size: 1rem;
font-style: italic;
color: var(--bc-21-ink);
line-height: 1.55;
max-width: 48ch;
}
@media (prefers-reduced-motion: reduce) {
.bc-21__wave path {
transition: opacity 160ms linear;
stroke-dasharray: none;
stroke-dashoffset: 0;
}
.bc-21__link {
transition: none;
}
}
@media (prefers-color-scheme: dark) {
.bc-21 {
background: oklch(0.19 0.01 70);
color: oklch(0.82 0.02 80);
--bc-21-ink: oklch(0.74 0.02 80);
--bc-21-ink-strong: oklch(0.93 0.02 85);
--bc-21-accent: oklch(0.78 0.14 60);
}
.bc-21__item + .bc-21__item::before {
color: oklch(0.55 0.02 70);
}
}.bc-21 {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(2rem, 6vw, 4rem) clamp(1rem, 4vw, 2.5rem);
background: oklch(0.97 0.01 85);
font-family: Charter, 'Iowan Old Style', 'Palatino Linotype', Georgia, 'Times New Roman', serif;
color: oklch(0.28 0.02 60);
-webkit-font-smoothing: antialiased;
--bc-21-ink: oklch(0.42 0.02 60);
--bc-21-ink-strong: oklch(0.22 0.02 60);
--bc-21-accent: oklch(0.55 0.14 30);
--bc-21-draw: 420ms;
--bc-21-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
.bc-21 *,
.bc-21 *::before,
.bc-21 *::after {
box-sizing: border-box;
}
.bc-21__frame {
width: 100%;
max-width: 62ch;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.bc-21__nav {
font-size: 0.9375rem;
line-height: 1.4;
}
.bc-21__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.15rem 0.35rem;
}
.bc-21__item {
display: inline-flex;
align-items: center;
min-height: 44px;
color: var(--bc-21-ink);
}
.bc-21__item + .bc-21__item::before {
content: "·";
margin: 0 0.55rem 0 0.2rem;
color: oklch(0.62 0.02 60);
font-size: 1.05em;
line-height: 1;
transform: translateY(-0.05em);
}
.bc-21__link {
position: relative;
display: inline-flex;
align-items: center;
padding: 0.6rem 0.15rem;
min-height: 44px;
color: var(--bc-21-ink);
text-decoration: none;
font-style: italic;
letter-spacing: 0.005em;
transition: color 220ms var(--bc-21-ease);
border-radius: 2px;
}
.bc-21__label {
position: relative;
display: inline-block;
}
.bc-21__wave {
position: absolute;
left: 0;
right: 0;
bottom: 0.35rem;
width: 100%;
height: 6px;
color: var(--bc-21-accent);
overflow: visible;
pointer-events: none;
}
.bc-21__wave path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
opacity: 0;
transition: stroke-dashoffset var(--bc-21-draw) var(--bc-21-ease), opacity 180ms var(--bc-21-ease);
}
.bc-21__link:hover,
.bc-21__link:focus-visible {
color: var(--bc-21-ink-strong);
}
.bc-21__link:hover .bc-21__wave path,
.bc-21__link:focus-visible .bc-21__wave path {
stroke-dashoffset: 0;
opacity: 1;
}
.bc-21__link:focus-visible {
outline: 2px solid var(--bc-21-accent);
outline-offset: 3px;
}
.bc-21__link--current {
color: var(--bc-21-ink-strong);
font-style: normal;
font-weight: 600;
cursor: default;
}
.bc-21__link--current .bc-21__wave {
display: none;
}
.bc-21__headline {
margin: 0.5rem 0 0;
font-size: clamp(1.75rem, 4vw, 2.4rem);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.01em;
color: var(--bc-21-ink-strong);
}
.bc-21__dek {
margin: 0;
font-size: 1rem;
font-style: italic;
color: var(--bc-21-ink);
line-height: 1.55;
max-width: 48ch;
}
@media (prefers-reduced-motion: reduce) {
.bc-21__wave path {
transition: opacity 160ms linear;
stroke-dasharray: none;
stroke-dashoffset: 0;
}
.bc-21__link {
transition: none;
}
}
@media (prefers-color-scheme: dark) {
.bc-21 {
background: oklch(0.19 0.01 70);
color: oklch(0.82 0.02 80);
--bc-21-ink: oklch(0.74 0.02 80);
--bc-21-ink-strong: oklch(0.93 0.02 85);
--bc-21-accent: oklch(0.78 0.14 60);
}
.bc-21__item + .bc-21__item::before {
color: oklch(0.55 0.02 70);
}
}/* No JavaScript — CSS transitions stroke-dashoffset from 100 to 0 on :hover and :focus-visible, drawing the wave underline in beneath each crumb. */
/* No JavaScript — CSS transitions stroke-dashoffset from 100 to 0 on :hover and :focus-visible, drawing the wave underline in beneath each crumb. */Here's a working CSS Breadcrumb from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Editorial Blog Breadcrumb with Animated Wave Underline Hover
Source: https://codefronts.com/navigation/css-breadcrumbs/wave-underline/
Editorial publishing breadcrumb in the spirit of Medium, Substack, and NYT — warm cream paper, muted ink serif type, and a signature hover flourish where an inline SVG wave underline draws itself beneath each crumb like a printer's flourish finishing on the page.
## HTML
```html
<section class="bc-21" aria-label="Editorial blog breadcrumb with wave underline demo">
<article class="bc-21__frame">
<nav aria-label="Breadcrumb" class="bc-21__nav">
<ol class="bc-21__list">
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Home</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Style</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Fashion</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item" aria-current="page">
<span class="bc-21__link bc-21__link--current">
<span class="bc-21__label">The Return of Tailored Denim</span>
</span>
</li>
</ol>
</nav>
<h1 class="bc-21__headline">The Return of Tailored Denim</h1>
<p class="bc-21__dek">A quiet revival of pressed seams and structured silhouettes, filed under Style / Fashion.</p>
</article>
</section>
```
## CSS
```css
.bc-21 {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(2rem, 6vw, 4rem) clamp(1rem, 4vw, 2.5rem);
background: oklch(0.97 0.01 85);
font-family: Charter, 'Iowan Old Style', 'Palatino Linotype', Georgia, 'Times New Roman', serif;
color: oklch(0.28 0.02 60);
-webkit-font-smoothing: antialiased;
--bc-21-ink: oklch(0.42 0.02 60);
--bc-21-ink-strong: oklch(0.22 0.02 60);
--bc-21-accent: oklch(0.55 0.14 30);
--bc-21-draw: 420ms;
--bc-21-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
.bc-21 *,
.bc-21 *::before,
.bc-21 *::after {
box-sizing: border-box;
}
.bc-21__frame {
width: 100%;
max-width: 62ch;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.bc-21__nav {
font-size: 0.9375rem;
line-height: 1.4;
}
.bc-21__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.15rem 0.35rem;
}
.bc-21__item {
display: inline-flex;
align-items: center;
min-height: 44px;
color: var(--bc-21-ink);
}
.bc-21__item + .bc-21__item::before {
content: "·";
margin: 0 0.55rem 0 0.2rem;
color: oklch(0.62 0.02 60);
font-size: 1.05em;
line-height: 1;
transform: translateY(-0.05em);
}
.bc-21__link {
position: relative;
display: inline-flex;
align-items: center;
padding: 0.6rem 0.15rem;
min-height: 44px;
color: var(--bc-21-ink);
text-decoration: none;
font-style: italic;
letter-spacing: 0.005em;
transition: color 220ms var(--bc-21-ease);
border-radius: 2px;
}
.bc-21__label {
position: relative;
display: inline-block;
}
.bc-21__wave {
position: absolute;
left: 0;
right: 0;
bottom: 0.35rem;
width: 100%;
height: 6px;
color: var(--bc-21-accent);
overflow: visible;
pointer-events: none;
}
.bc-21__wave path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
opacity: 0;
transition: stroke-dashoffset var(--bc-21-draw) var(--bc-21-ease), opacity 180ms var(--bc-21-ease);
}
.bc-21__link:hover,
.bc-21__link:focus-visible {
color: var(--bc-21-ink-strong);
}
.bc-21__link:hover .bc-21__wave path,
.bc-21__link:focus-visible .bc-21__wave path {
stroke-dashoffset: 0;
opacity: 1;
}
.bc-21__link:focus-visible {
outline: 2px solid var(--bc-21-accent);
outline-offset: 3px;
}
.bc-21__link--current {
color: var(--bc-21-ink-strong);
font-style: normal;
font-weight: 600;
cursor: default;
}
.bc-21__link--current .bc-21__wave {
display: none;
}
.bc-21__headline {
margin: 0.5rem 0 0;
font-size: clamp(1.75rem, 4vw, 2.4rem);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.01em;
color: var(--bc-21-ink-strong);
}
.bc-21__dek {
margin: 0;
font-size: 1rem;
font-style: italic;
color: var(--bc-21-ink);
line-height: 1.55;
max-width: 48ch;
}
@media (prefers-reduced-motion: reduce) {
.bc-21__wave path {
transition: opacity 160ms linear;
stroke-dasharray: none;
stroke-dashoffset: 0;
}
.bc-21__link {
transition: none;
}
}
@media (prefers-color-scheme: dark) {
.bc-21 {
background: oklch(0.19 0.01 70);
color: oklch(0.82 0.02 80);
--bc-21-ink: oklch(0.74 0.02 80);
--bc-21-ink-strong: oklch(0.93 0.02 85);
--bc-21-accent: oklch(0.78 0.14 60);
}
.bc-21__item + .bc-21__item::before {
color: oklch(0.55 0.02 70);
}
}
```
## JavaScript
```js
/* No JavaScript — CSS transitions stroke-dashoffset from 100 to 0 on :hover and :focus-visible, drawing the wave underline in beneath each crumb. */
```Here's a working CSS Breadcrumb from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Editorial Blog Breadcrumb with Animated Wave Underline Hover
Source: https://codefronts.com/navigation/css-breadcrumbs/wave-underline/
Editorial publishing breadcrumb in the spirit of Medium, Substack, and NYT — warm cream paper, muted ink serif type, and a signature hover flourish where an inline SVG wave underline draws itself beneath each crumb like a printer's flourish finishing on the page.
## HTML
```html
<section class="bc-21" aria-label="Editorial blog breadcrumb with wave underline demo">
<article class="bc-21__frame">
<nav aria-label="Breadcrumb" class="bc-21__nav">
<ol class="bc-21__list">
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Home</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Style</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item">
<a class="bc-21__link" href="#">
<span class="bc-21__label">Fashion</span>
<svg class="bc-21__wave" aria-hidden="true" viewBox="0 0 60 6" preserveAspectRatio="none">
<path d="M0 3 Q 15 0, 30 3 T 60 3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" pathLength="100" vector-effect="non-scaling-stroke"/>
</svg>
</a>
</li>
<li class="bc-21__item" aria-current="page">
<span class="bc-21__link bc-21__link--current">
<span class="bc-21__label">The Return of Tailored Denim</span>
</span>
</li>
</ol>
</nav>
<h1 class="bc-21__headline">The Return of Tailored Denim</h1>
<p class="bc-21__dek">A quiet revival of pressed seams and structured silhouettes, filed under Style / Fashion.</p>
</article>
</section>
```
## CSS
```css
.bc-21 {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(2rem, 6vw, 4rem) clamp(1rem, 4vw, 2.5rem);
background: oklch(0.97 0.01 85);
font-family: Charter, 'Iowan Old Style', 'Palatino Linotype', Georgia, 'Times New Roman', serif;
color: oklch(0.28 0.02 60);
-webkit-font-smoothing: antialiased;
--bc-21-ink: oklch(0.42 0.02 60);
--bc-21-ink-strong: oklch(0.22 0.02 60);
--bc-21-accent: oklch(0.55 0.14 30);
--bc-21-draw: 420ms;
--bc-21-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
.bc-21 *,
.bc-21 *::before,
.bc-21 *::after {
box-sizing: border-box;
}
.bc-21__frame {
width: 100%;
max-width: 62ch;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.bc-21__nav {
font-size: 0.9375rem;
line-height: 1.4;
}
.bc-21__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.15rem 0.35rem;
}
.bc-21__item {
display: inline-flex;
align-items: center;
min-height: 44px;
color: var(--bc-21-ink);
}
.bc-21__item + .bc-21__item::before {
content: "·";
margin: 0 0.55rem 0 0.2rem;
color: oklch(0.62 0.02 60);
font-size: 1.05em;
line-height: 1;
transform: translateY(-0.05em);
}
.bc-21__link {
position: relative;
display: inline-flex;
align-items: center;
padding: 0.6rem 0.15rem;
min-height: 44px;
color: var(--bc-21-ink);
text-decoration: none;
font-style: italic;
letter-spacing: 0.005em;
transition: color 220ms var(--bc-21-ease);
border-radius: 2px;
}
.bc-21__label {
position: relative;
display: inline-block;
}
.bc-21__wave {
position: absolute;
left: 0;
right: 0;
bottom: 0.35rem;
width: 100%;
height: 6px;
color: var(--bc-21-accent);
overflow: visible;
pointer-events: none;
}
.bc-21__wave path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
opacity: 0;
transition: stroke-dashoffset var(--bc-21-draw) var(--bc-21-ease), opacity 180ms var(--bc-21-ease);
}
.bc-21__link:hover,
.bc-21__link:focus-visible {
color: var(--bc-21-ink-strong);
}
.bc-21__link:hover .bc-21__wave path,
.bc-21__link:focus-visible .bc-21__wave path {
stroke-dashoffset: 0;
opacity: 1;
}
.bc-21__link:focus-visible {
outline: 2px solid var(--bc-21-accent);
outline-offset: 3px;
}
.bc-21__link--current {
color: var(--bc-21-ink-strong);
font-style: normal;
font-weight: 600;
cursor: default;
}
.bc-21__link--current .bc-21__wave {
display: none;
}
.bc-21__headline {
margin: 0.5rem 0 0;
font-size: clamp(1.75rem, 4vw, 2.4rem);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.01em;
color: var(--bc-21-ink-strong);
}
.bc-21__dek {
margin: 0;
font-size: 1rem;
font-style: italic;
color: var(--bc-21-ink);
line-height: 1.55;
max-width: 48ch;
}
@media (prefers-reduced-motion: reduce) {
.bc-21__wave path {
transition: opacity 160ms linear;
stroke-dasharray: none;
stroke-dashoffset: 0;
}
.bc-21__link {
transition: none;
}
}
@media (prefers-color-scheme: dark) {
.bc-21 {
background: oklch(0.19 0.01 70);
color: oklch(0.82 0.02 80);
--bc-21-ink: oklch(0.74 0.02 80);
--bc-21-ink-strong: oklch(0.93 0.02 85);
--bc-21-accent: oklch(0.78 0.14 60);
}
.bc-21__item + .bc-21__item::before {
color: oklch(0.55 0.02 70);
}
}
```
## JavaScript
```js
/* No JavaScript — CSS transitions stroke-dashoffset from 100 to 0 on :hover and :focus-visible, drawing the wave underline in beneath each crumb. */
```How this works
Each crumb link contains an inline SVG with a single quadratic-bezier path (M0 3 Q 15 0, 30 3 T 60 3) drawn in currentColor. The path uses the pathLength="100" attribute so stroke-dasharray and stroke-dashoffset can be reasoned about as percentages instead of pixel arc-lengths. At rest the wave sits at stroke-dashoffset: 100 — fully retracted, invisible. On :hover and :focus-visible the dashoffset transitions to 0 across ~420ms with a soft cubic-bezier ease-out, drawing the wave in left-to-right like ink being laid down. The SVG is absolutely positioned beneath the text baseline so it never disturbs the crumb's own line-box.
Typography leans on a Georgia-first serif stack (Charter, 'Iowan Old Style', Georgia, serif) sized at 15px with generous letter-spacing to feel like set-in-print body copy. The warm oklch cream background pairs with a muted ink foreground for that broadsheet feel. Semantics stay strict: <nav aria-label="Breadcrumb"> wraps an ordered list, ::before pseudos render the · separators (hidden from AT via role/aria patterns baked into the list structure), and the final crumb carries aria-current="page". Every link has a 44px hit target through vertical padding, a visible :focus-visible ring, and a prefers-reduced-motion guard that flips the wave to a static fade instead of the draw-in animation.
Make it yours
- Swap the wave amplitude by editing the SVG path — `Q 15 -1, 30 3 T 60 3` gives a taller crest for a more playful flourish.
- Change ink color via the `--bc-21-ink` custom property; the wave inherits through `currentColor` automatically.
- Slow or quicken the draw by adjusting the `--bc-21-draw` duration token (default 420ms) — 700ms feels calligraphic.
- For a dropcap on Home, uncomment the `.bc-21__item:first-child .bc-21__link::first-letter` block to enlarge the H.
Gotchas — read before shipping
- Set `stroke-width` in absolute units and pair with `vector-effect: non-scaling-stroke` — otherwise the wave thins visibly when users zoom or on hi-dpi Retina rendering.
- The `pathLength="100"` attribute normalizes the arc so `stroke-dasharray: 100` works — omit it and the dashoffset math depends on the real Bezier arc length, which varies per browser.
- Always ship a Georgia fallback in the serif stack; Charter and Iowan Old Style only exist on Apple platforms, and a bare `serif` keyword lands on Times New Roman on Windows, which reads too newsprint-heavy.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 113+ | 111+ |
Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 56+.
SVG stroke-dasharray transitions are baseline everywhere since 2018. The stroke-dashoffset animation is the workhorse — no JS, no @property, no view-timeline required.