Feature 312 Layout & theme

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; }
}

Are you sure?

Rename

↑↓ moveEnter openEsc close