Every site that cares about orienting users relies on breadcrumbs. A clean breadcrumb trail with crisp chevrons offers clarity without noise. In this tutorial you will build fully responsive, accessible breadcrumbs that draw their “greater-than” chevrons purely with CSS. No images, no SVGs, and no extra markup clutter. By the end, you will have a component you can drop into any project and theme in minutes.
Why Building Breadcrumbs with CSS Chevrons Matters
Breadcrumbs communicate hierarchy and reduce backtracking. They must be readable, compact, and consistent with your design system. Rendering chevron separators in CSS means zero image requests, resolution independence, easy color theming, and fewer moving parts. You also gain fine control of thickness, spacing, and animation with a couple of custom properties. A CSS-first approach also keeps your HTML semantic and your UI scalable.
Prerequisites
You will follow a straightforward CSS-first pattern. The only dependencies are the browser features you already use daily.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The markup is semantic and minimal. A nav with an aria-label encloses an ordered list. Each crumb is a list item that contains either an anchor (for navigable steps) or a span flagged as the current page. The separators will be generated in CSS, so there is no need for extra nodes.
<!-- HTML -->
<nav class="breadcrumb-nav" aria-label="Breadcrumb">
<ol class="breadcrumbs">
<li class="crumb"><a href="/">Home</a></li>
<li class="crumb"><a href="/docs">Docs</a></li>
<li class="crumb"><a href="/docs/components">Components</a></li>
<li class="crumb"><span aria-current="page">Breadcrumbs</span></li>
</ol>
</nav>
Keeping the separator out of the DOM means assistive tech will not read out “greater than” symbols between labels. Screen reader users hear only the meaningful text and structure. The last crumb uses aria-current to clearly mark the active location.
Step 2: The Basic CSS & Styling
Set up a small design system with CSS variables for spacing, colors, radius, chevron size, and stroke thickness. These variables drive the layout and let you theme the widget from a single place.
/* CSS */
:root {
--surface: #ffffff;
--text: #0f172a;
--crumb-bg: #f3f4f6;
--crumb-fg: #111827;
--crumb-bg-hover: #e5e7eb;
--active-bg: #4f46e5;
--active-fg: #ffffff;
--sep-color: #9ca3af; /* chevron stroke color */
--chev-size: 8px; /* size from tip to inner joint */
--chev-stroke: 2px; /* thickness of chevron arms */
--radius: 8px;
--y-pad: 0.5rem;
--x-pad: 0.75rem;
--gap-x: 1rem;
--focus: #22c55e;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji", "Segoe UI Emoji";
line-height: 1.5;
margin: 0;
color: var(--text);
background: var(--surface);
}
.breadcrumb-nav {
padding: 1rem;
overflow-x: auto; /* graceful overflow on small screens */
}
.breadcrumbs {
display: flex;
align-items: center;
gap: 0; /* separators are absolutely positioned */
list-style: none;
padding: 0;
margin: 0;
white-space: nowrap; /* keep labels on one line */
}
.crumb {
position: relative; /* anchor separators */
display: flex;
align-items: center;
}
.crumb + .crumb {
margin-left: var(--gap-x);
}
.crumb a,
.crumb span {
display: block;
text-decoration: none;
color: var(--crumb-fg);
background: var(--crumb-bg);
padding: var(--y-pad) var(--x-pad);
border-radius: var(--radius);
}
.crumb a:hover {
background: var(--crumb-bg-hover);
}
.crumb a:focus-visible,
.crumb span:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.crumb span[aria-current="page"] {
background: var(--active-bg);
color: var(--active-fg);
font-weight: 600;
}
Advanced Tip: Keep every visual choice in a custom property. You can theme the entire component by swapping the variable block on a parent. For dark mode, set a different –surface, swap –crumb-bg and –sep-color, and the chevrons and pills will stay in sync.
Step 3: Building the Chevron Separators
The chevrons sit between items and are built with two layered triangles. The first triangle draws the outer “>” color. The second triangle, slightly smaller and nudged to the right, punches out the center using the page background color, leaving a neat, crisp chevron stroke.
/* CSS */
.crumb + .crumb::before,
.crumb + .crumb::after {
content: "";
position: absolute;
left: calc(-0.75rem); /* pull the chevron into the gap */
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-style: solid;
pointer-events: none; /* decorative only */
}
/* Outer triangle (stroke color) */
.crumb + .crumb::before {
border-width: var(--chev-size) 0 var(--chev-size) var(--chev-size);
border-color: transparent transparent transparent var(--sep-color);
}
/* Inner triangle (carves out the middle to form >) */
.crumb + .crumb::after {
border-width: calc(var(--chev-size) - var(--chev-stroke)) 0
calc(var(--chev-size) - var(--chev-stroke))
calc(var(--chev-size) - var(--chev-stroke));
border-color: transparent transparent transparent var(--surface);
transform: translateY(-50%) translateX(1px);
}
How This Works (Code Breakdown)
The separator is attached to every item that follows another item using the adjacent sibling selector. Position: relative on each .crumb creates an anchor for the pseudo-elements and keeps stacking straightforward. The chevron is drawn at the vertical center with translateY(-50%), so the arms stay centered no matter the font size or padding.
Each triangle uses the classic border trick. When an element has zero width and height but solid borders, the visible portion is a triangle pointing toward the side with a non-transparent border. The ::before triangle points right by filling the left border and clearing the top, right, and bottom borders. The ::after triangle is a smaller, same-direction triangle with the background color. Shifting the inner triangle by 1px to the right leaves an even stroke on both arms and avoids sub-pixel blur. If you want a refresher on the technique, see how a triangle right with CSS is built from borders.
The stroke thickness is controlled by –chev-stroke. Increase it for a bold separator or decrease it for a finer look. The overall size of the chevron changes with –chev-size. This separation of concern is the reason custom properties pay off in production. If you would rather draw a standalone icon for other UI parts, the method behind a chevron right with CSS maps directly to what the pseudo-elements are doing here.
Step 4: Shaping, Spacing, and States
Now refine spacing, focus, and active states. The goal is a large tap target, clear focus ring, and a glossy current-page pill that stands out. Keep separators legible over both light and dark surfaces. The code below adds subtle transitions, a pressed state, and improves mobile behavior with scroll snapping.
/* CSS */
.crumb a,
.crumb span {
transition: background-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.crumb a:active {
box-shadow: inset 0 1px 3px rgba(0,0,0,0.15);
}
.breadcrumb-nav {
scroll-snap-type: x proximity;
}
.crumb {
scroll-snap-align: end;
}
/* High-contrast theme variant example */
@media (prefers-color-scheme: dark) {
:root {
--surface: #0b1020;
--text: #e5e7eb;
--crumb-bg: #111827;
--crumb-fg: #e5e7eb;
--crumb-bg-hover: #1f2937;
--active-bg: #7c3aed;
--active-fg: #ffffff;
--sep-color: #9ca3af;
--focus: #34d399;
}
/* Recolor inner triangle to match new surface */
.crumb + .crumb::after {
border-left-color: var(--surface);
}
}
/* Focus visibility on keyboard only */
:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
How This Works (Code Breakdown)
Transitions are limited to color and shadow for smoothness. These are inexpensive properties that do not trigger layout thrash. The pressed state uses an inset shadow to give clear feedback on touch devices. The nav is horizontally scrollable with overflow-x: auto and scroll-snap, which helps keep items aligned when the trail is long. This avoids truncating labels and keeps the control usable on narrow viewports.
Dark mode swaps the surface and crumb colors and updates the inner triangle’s color to match the new background so the chevron keeps its cutout effect. You can take the same approach to theme other contexts, like a hero section with a photo background or a sidebar with a tinted surface.
For teams that prefer simply shaped blocks, the crumbs here are rectangular pills. If you want a refresher on drawing that basic shape cleanly and controlling its corners, see how to make a rectangle with CSS.
Advanced Techniques: Animations & Hover Effects
Chevrons can convey direction on hover by nudging the next separator or fading its stroke color. Keep motion subtle and respect reduced motion settings. The snippet below fades the separator and moves it by 1px when the preceding crumb is hovered. This connects the hover state with the direction of travel.
/* CSS */
.crumb:hover + .crumb::before {
border-left-color: color-mix(in srgb, var(--sep-color), var(--active-bg) 45%);
}
.crumb:hover + .crumb::after {
transform: translateY(-50%) translateX(2px);
transition: transform 160ms ease, border-left-color 160ms ease;
}
/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
.crumb a,
.crumb span,
.crumb:hover + .crumb::after {
transition: none;
}
}
The hover rule targets the next item’s chevron using the adjacent sibling combinator. The color-mix function tints the stroke toward the active color to indicate intent, but the effect still reads well on both light and dark schemes. The prefers-reduced-motion query disables hover motion for users who prefer a calmer interface.
Accessibility & Performance
This pattern stays readable, keyboard-friendly, and screen reader friendly while keeping the paint work cheap.
Accessibility
Use nav with aria-label=”Breadcrumb” so assistive tech identifies the region. An ordered list communicates that the trail is sequential. Mark the current view with aria-current=”page” on the last crumb’s element. Relative font sizing keeps the tap target large enough on mobile. The chevrons are pseudo-elements without text content, so they are ignored by screen readers. Provide a visible focus state with outline so keyboard users can track position across the trail. Respect reduced motion for hover animations.
Performance
The component is CSS-only. Pseudo-elements draw simple triangles, which are cheap to render. There are no layout-triggering animations, just color and transform. The small amount of absolute positioning does not fight layout, and custom properties allow runtime theming without rewriting large CSS blocks. Avoid heavy blurs or layered shadows on every crumb, as these degrade on low-end GPUs. Keep shadows shallow and transitions short.
Ship Breadcrumbs You Can Trust
You built a breadcrumb component that renders crisp chevrons with layered triangles, scales with your typography, and respects user preferences. The structure remains semantic, the separators are decorative, and theming is a set of variable overrides. Now you have the tools to ship a clear trail in any design system and extend the pattern wherever a chevron separator fits.