Breadcrumb Navigation เส้นทางนำทาง
A trail of links that shows where you are; built on the server from the request and the database.
Live demo
Everything here talks to the real Spring Boot + SQL Server back end.Browse the product catalogue
Look at the top bar while you click: the controller adds one crumb per level (lesson → category → product).
Style variants of the same trail
What to notice
- The controller returns a List<Crumb>; the layout renders it for every page.
- The last crumb is plain text with aria-current=page.
- Long trails collapse the middle items into '...' on small screens.
Try it
Source code
5 parts, read from the files in this project
src/main/java/com/study/uiux/web/PageController.java
A breadcrumb item: the last one has no link
public record Crumb(String label, String href) {
}
src/main/java/com/study/uiux/web/PageController.java
Build the trail from the URL and the database
List<Crumb> crumbs = new ArrayList<>();
crumbs.add(new Crumb("Home", "/"));
crumbs.add(new Crumb(lesson.group(), "/#" + LessonCatalog.slug(lesson.group())));
if (no == 312 && category != null) {
crumbs.add(new Crumb(lesson.name(), lesson.url()));
Product selected = product == null ? null : products.findById(product).orElse(null);
if (selected != null) {
crumbs.add(new Crumb(selected.getCategory(), lesson.url() + "?category=" + selected.getCategory()));
crumbs.add(new Crumb(selected.getName(), null));
model.addAttribute("selected", selected);
} else {
crumbs.add(new Crumb(category, null));
}
model.addAttribute("category", selected != null ? selected.getCategory() : category);
} else {
crumbs.add(new Crumb(lesson.no() + " " + lesson.name(), null));
}
model.addAttribute("crumbs", crumbs);
src/main/resources/db/migration/V1__schema.sql
product table
CREATE TABLE product (
id INT IDENTITY(1,1) NOT NULL CONSTRAINT pk_product PRIMARY KEY,
name NVARCHAR(120) NOT NULL,
category NVARCHAR(40) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL,
rating DECIMAL(3,1) NOT NULL,
art_seed INT NOT NULL,
description NVARCHAR(400) NOT NULL,
created_at DATETIME2(0) NOT NULL CONSTRAINT df_product_created DEFAULT SYSUTCDATETIME()
);
CREATE INDEX ix_product_category ON product (category, name);
src/main/resources/templates/layout.html
Breadcrumb rendered from the List<Crumb> the controller returned
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol th:classappend="${#lists.size(crumbs) > 3} ? 'long'">
<li th:each="c, s : ${crumbs}" th:classappend="${!s.first and !s.last and s.count < s.size - 1} ? 'middle'">
<a th:if="${c.href != null}" th:href="${c.href}" th:text="${c.label}">Home</a>
<span th:if="${c.href == null}" aria-current="page" th:text="${c.label}">Page</span>
</li>
<li class="crumb-ellipsis" aria-hidden="true">…</li>
</ol>
</nav>
src/main/resources/static/css/app.css
Breadcrumb: chevrons between items, the middle collapses on phones
.breadcrumb { min-width: 0; flex: 1; }
.breadcrumb ol { display: flex; align-items: center; list-style: none; margin: 0; padding: 0; font-size: .875rem; min-width: 0; }
.breadcrumb li { display: flex; align-items: center; min-width: 0; order: 2; }
.breadcrumb li:first-child { order: 0; }
.breadcrumb li + li::before {
content: ""; width: .45rem; height: .45rem; margin: 0 .6rem; flex: none; border-right: 1.5px solid var(--text-3); border-top: 1.5px solid var(--text-3);
transform: rotate(45deg); opacity: .7;
}
.breadcrumb a { color: var(--text-2); text-decoration: none; white-space: nowrap; padding: .15rem .3rem; border-radius: .35rem; }
.breadcrumb a:hover { color: var(--text); background: var(--surface-2); }
.breadcrumb [aria-current] { color: var(--text); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.breadcrumb .crumb-ellipsis { display: none; order: 1; color: var(--text-3); }
@media (max-width: 720px) {
.breadcrumb ol.long li.middle { display: none; }
.breadcrumb ol.long .crumb-ellipsis { display: flex; }
}