Museum — Historical Timeline
A curatorial historical timeline that walks five centuries of art movements as colour-coded era bands and dated milestone markers. A scrubber travels through the years and highlights the active era, while click-to-expand cards open a typeset museum label with artist, medium, dimensions, catalogue number and credit line. Era chips filter the track, arrow keys step between works, and the horizontal rail folds into a vertical timeline on mobile — refined serif display type, soft gallery shadows and inline plate imagery throughout.
MCP
Code
:root {
--paper: #f6f4ef;
--wall: #ffffff;
--charcoal: #1c1b19;
--ink: #2a2825;
--ink-2: #4a4640;
--muted: #8c857a;
--gold: #a98140;
--gold-d: #876631;
--gold-50: #f3ecdd;
--line: rgba(28, 27, 25, 0.12);
--line-2: rgba(28, 27, 25, 0.2);
--ok: #3f7d56;
--warn: #b8842c;
--danger: #b4493a;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--baroque: #7a5b8e;
--romantic: #9c5b3f;
--impressionist: #4a86a8;
--modern: #b8842c;
--contemporary: #3f7d56;
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.frame {
max-width: 1100px;
margin: 0 auto;
padding: 44px 28px 64px;
}
/* ---- Masthead ---- */
.masthead { margin-bottom: 32px; }
.masthead__brand {
display: flex;
align-items: center;
gap: 16px;
color: var(--gold-d);
}
.sigil { display: inline-flex; flex: none; }
.masthead__eyebrow {
margin: 0;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
font-weight: 500;
}
.masthead__title {
margin: 2px 0 0;
font-family: var(--serif);
font-weight: 600;
font-size: clamp(2rem, 5vw, 3.1rem);
line-height: 1.04;
color: var(--charcoal);
letter-spacing: 0.01em;
}
.masthead__lede {
margin: 18px 0 0;
max-width: 60ch;
color: var(--ink-2);
font-size: 1.02rem;
}
/* ---- Era chips ---- */
.eras {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 18px 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
margin-bottom: 26px;
}
.era-chip {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--sans);
font-size: 0.84rem;
font-weight: 500;
color: var(--ink-2);
background: var(--wall);
border: 1px solid var(--line);
border-radius: 999px;
padding: 7px 15px;
cursor: pointer;
transition: border-color 0.18s, color 0.18s, box-shadow 0.18s, background 0.18s;
}
.era-chip:hover { border-color: var(--line-2); color: var(--ink); }
.era-chip.is-active {
color: var(--charcoal);
border-color: var(--gold);
background: var(--gold-50);
box-shadow: 0 1px 0 rgba(169, 129, 64, 0.25);
}
.era-chip:focus-visible {
outline: 2px solid var(--gold-d);
outline-offset: 2px;
}
.era-chip__dot {
width: 9px; height: 9px;
border-radius: 50%;
flex: none;
}
.era-chip__dot[data-era="baroque"] { background: var(--baroque); }
.era-chip__dot[data-era="romantic"] { background: var(--romantic); }
.era-chip__dot[data-era="impressionist"] { background: var(--impressionist); }
.era-chip__dot[data-era="modern"] { background: var(--modern); }
.era-chip__dot[data-era="contemporary"] { background: var(--contemporary); }
/* ---- Scrubber ---- */
.scrubber {
position: relative;
margin-bottom: 38px;
padding: 6px 2px 0;
}
.scrubber__bands {
position: relative;
display: flex;
height: 26px;
border-radius: var(--r-sm);
overflow: hidden;
border: 1px solid var(--line);
margin-bottom: 14px;
}
.scrubber__band {
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.62rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.92);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.2s;
}
.scrubber__band.is-dim { opacity: 0.28; }
.scrubber__range {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
border-radius: 4px;
background: var(--line-2);
cursor: pointer;
margin: 6px 0 0;
}
.scrubber__range:focus-visible { outline: 2px solid var(--gold-d); outline-offset: 6px; }
.scrubber__range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px; height: 20px;
border-radius: 50%;
background: var(--wall);
border: 2px solid var(--gold-d);
box-shadow: 0 2px 6px rgba(28, 27, 25, 0.22);
cursor: grab;
}
.scrubber__range::-moz-range-thumb {
width: 20px; height: 20px;
border-radius: 50%;
background: var(--wall);
border: 2px solid var(--gold-d);
box-shadow: 0 2px 6px rgba(28, 27, 25, 0.22);
cursor: grab;
}
.scrubber__readout {
display: flex;
align-items: baseline;
gap: 14px;
margin-top: 12px;
}
.scrubber__year {
font-family: var(--serif);
font-size: 1.6rem;
font-weight: 600;
color: var(--charcoal);
font-variant-numeric: tabular-nums;
}
.scrubber__hint {
font-size: 0.78rem;
color: var(--muted);
}
/* ---- Timeline ---- */
.timeline {
position: relative;
outline: none;
}
.timeline:focus-visible { outline: 2px solid var(--gold-d); outline-offset: 8px; border-radius: var(--r-md); }
.timeline__rail {
position: absolute;
left: 0; right: 0;
top: 60px;
height: 2px;
background: linear-gradient(90deg, transparent, var(--line-2) 6%, var(--line-2) 94%, transparent);
}
.track {
list-style: none;
display: flex;
gap: 18px;
overflow-x: auto;
padding: 0 2px 22px;
margin: 0;
scroll-snap-type: x proximity;
scrollbar-width: thin;
scrollbar-color: var(--line-2) transparent;
}
.track::-webkit-scrollbar { height: 8px; }
.track::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 8px; }
.milestone {
flex: 0 0 230px;
scroll-snap-align: start;
position: relative;
padding-top: 96px;
transition: opacity 0.25s, filter 0.25s;
}
.milestone.is-faded { opacity: 0.26; filter: saturate(0.6); }
.milestone__marker {
position: absolute;
top: 52px;
left: 22px;
width: 18px; height: 18px;
border-radius: 50%;
background: var(--wall);
border: 3px solid var(--era);
box-shadow: 0 0 0 5px var(--paper);
z-index: 2;
}
.milestone__year {
position: absolute;
top: 14px;
left: 0;
font-family: var(--serif);
font-size: 1.5rem;
font-weight: 600;
color: var(--charcoal);
font-variant-numeric: tabular-nums;
}
.milestone__card {
width: 100%;
text-align: left;
background: var(--wall);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 0;
cursor: pointer;
overflow: hidden;
font-family: var(--sans);
color: inherit;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
box-shadow: 0 1px 2px rgba(28, 27, 25, 0.04);
}
.milestone__card:hover {
transform: translateY(-4px);
box-shadow: 0 14px 30px rgba(28, 27, 25, 0.12);
border-color: var(--line-2);
}
.milestone__card:focus-visible {
outline: 2px solid var(--gold-d);
outline-offset: 3px;
}
.milestone.is-open .milestone__card {
border-color: var(--gold);
box-shadow: 0 14px 34px rgba(169, 129, 64, 0.2);
}
.milestone__plate {
height: 116px;
position: relative;
border-bottom: 1px solid var(--line);
}
.milestone__plate::after {
content: "";
position: absolute;
inset: 8px;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 3px;
pointer-events: none;
}
.milestone__text { padding: 13px 15px 16px; }
.milestone__badge {
display: inline-block;
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
font-weight: 600;
color: var(--era);
margin-bottom: 7px;
}
.milestone__title {
margin: 0;
font-family: var(--serif);
font-size: 1.22rem;
font-weight: 600;
line-height: 1.15;
color: var(--charcoal);
}
.milestone__artist {
margin: 4px 0 0;
font-size: 0.82rem;
color: var(--muted);
font-style: italic;
}
/* ---- Detail card ---- */
.detail {
margin-top: 30px;
animation: rise 0.32s ease both;
}
@keyframes rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.detail__card {
position: relative;
display: grid;
grid-template-columns: 220px 1fr;
gap: 28px;
background: var(--wall);
border: 1px solid var(--line);
border-left: 4px solid var(--era, var(--gold));
border-radius: var(--r-lg);
padding: 28px;
box-shadow: 0 18px 40px rgba(28, 27, 25, 0.1);
}
.detail__close {
position: absolute;
top: 14px; right: 16px;
width: 34px; height: 34px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--paper);
color: var(--ink-2);
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
transition: background 0.18s, color 0.18s, border-color 0.18s;
}
.detail__close:hover { background: var(--gold-50); color: var(--charcoal); border-color: var(--gold); }
.detail__close:focus-visible { outline: 2px solid var(--gold-d); outline-offset: 2px; }
.detail__plate {
height: 220px;
border-radius: var(--r-md);
border: 1px solid var(--line);
position: relative;
}
.detail__plate::after {
content: "";
position: absolute;
inset: 14px;
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 4px;
}
.detail__era {
margin: 0 0 4px;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
font-weight: 600;
color: var(--era, var(--gold-d));
}
.detail__year {
margin: 0;
font-family: var(--serif);
font-size: 1.05rem;
font-weight: 600;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.detail__title {
margin: 6px 0 2px;
font-family: var(--serif);
font-size: clamp(1.7rem, 3.6vw, 2.3rem);
font-weight: 600;
line-height: 1.08;
color: var(--charcoal);
}
.detail__artist {
margin: 0 0 16px;
font-size: 0.96rem;
color: var(--ink-2);
font-style: italic;
}
.detail__meta {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 20px;
margin: 0 0 16px;
font-size: 0.88rem;
}
.detail__meta dt {
color: var(--muted);
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
align-self: center;
}
.detail__meta dd {
margin: 0;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.detail__note {
margin: 0;
padding-top: 14px;
border-top: 1px solid var(--line);
color: var(--ink-2);
font-size: 0.95rem;
max-width: 56ch;
}
/* ---- Colophon ---- */
.colophon {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 8px;
margin-top: 40px;
padding-top: 18px;
border-top: 1px solid var(--line);
font-size: 0.78rem;
color: var(--muted);
}
.colophon__count { font-weight: 500; color: var(--ink-2); }
/* ---- Toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(20px);
background: var(--charcoal);
color: var(--paper);
padding: 11px 20px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
box-shadow: 0 10px 28px rgba(28, 27, 25, 0.3);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 60;
}
.toast.is-on {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ---- Responsive: vertical timeline ---- */
@media (max-width: 520px) {
.frame { padding: 30px 16px 50px; }
.masthead__lede { font-size: 0.95rem; }
.timeline__rail {
top: 0; bottom: 0; left: 9px; right: auto;
width: 2px; height: auto;
background: linear-gradient(180deg, transparent, var(--line-2) 4%, var(--line-2) 96%, transparent);
}
.track {
flex-direction: column;
overflow-x: visible;
gap: 26px;
padding-left: 30px;
}
.milestone {
flex: none;
padding-top: 0;
padding-left: 6px;
}
.milestone__marker {
top: 6px;
left: -27px;
}
.milestone__year {
position: static;
margin-bottom: 8px;
display: block;
font-size: 1.3rem;
}
.detail__card {
grid-template-columns: 1fr;
gap: 18px;
padding: 22px 18px;
}
.detail__plate { height: 170px; }
.detail__meta { grid-template-columns: auto 1fr; gap: 5px 14px; }
}(function () {
"use strict";
var ERAS = {
baroque: { label: "Baroque", color: "#7a5b8e", from: 1600, to: 1740 },
romantic: { label: "Romanticism", color: "#9c5b3f", from: 1780, to: 1860 },
impressionist: { label: "Impressionism", color: "#4a86a8", from: 1860, to: 1905 },
modern: { label: "Modernism", color: "#b8842c", from: 1905, to: 1965 },
contemporary: { label: "Contemporary", color: "#3f7d56", from: 1965, to: 2025 }
};
var BAND_ORDER = ["baroque", "romantic", "impressionist", "modern", "contemporary"];
var MILESTONES = [
{ year: 1621, era: "baroque", title: "Supper at Emmaus, after the Master", artist: "Workshop of Lievin de Vos",
medium: "Oil on oak panel", dims: "94 × 121 cm", cat: "ALD.1621.04", credit: "Bequest of M. Aldworth, 1903",
note: "An early devotional canvas pressing dramatic chiaroscuro into a domestic interior — the cornerstone of the collection's Baroque holdings.", plate: ["#2c2418", "#7a5b8e"] },
{ year: 1668, era: "baroque", title: "The Cartographer's Table", artist: "Anneke Brouwer",
medium: "Oil on canvas", dims: "71 × 58 cm", cat: "ALD.1668.11", credit: "Purchase, the Founders' Fund",
note: "A quiet still life of instruments and parchment, prized for its raking northern light and meticulous tonal recession.", plate: ["#3a2e1c", "#8f6a4a"] },
{ year: 1804, era: "romantic", title: "Storm over the Salt Marsh", artist: "Edmund Hale",
medium: "Oil on canvas", dims: "112 × 168 cm", cat: "ALD.1804.02", credit: "Gift of the Hale family, 1958",
note: "Romanticism's sublime made literal: weather as feeling. The horizon dissolves into vapour above a flooded coastal flat.", plate: ["#5a3a28", "#9c5b3f"] },
{ year: 1839, era: "romantic", title: "The Wanderer at Greylock", artist: "Edmund Hale",
medium: "Oil on canvas", dims: "98 × 76 cm", cat: "ALD.1839.07", credit: "Purchase, 1971",
note: "A lone figure dwarfed by ridge and cloud — the artist's most reproduced meditation on solitude and scale.", plate: ["#4a3324", "#b07a52"] },
{ year: 1874, era: "impressionist", title: "Quai des Lilas, Morning", artist: "Hélène Marchand",
medium: "Oil on canvas", dims: "60 × 73 cm", cat: "ALD.1874.13", credit: "Bequest of C. Verlaine, 1929",
note: "Painted en plein air over three damp mornings; broken touches of violet and pale gold dissolve the riverbank into light.", plate: ["#3a5a6e", "#9fc2d2"] },
{ year: 1888, era: "impressionist", title: "The Conservatory Window", artist: "Hélène Marchand",
medium: "Pastel on laid paper", dims: "48 × 39 cm", cat: "ALD.1888.05", credit: "Acquired with assistance of the Friends",
note: "Glass, fern and reflected sky rendered almost entirely in chalk — a tour de force of atmospheric pastel.", plate: ["#4a86a8", "#cfe2ea"] },
{ year: 1912, era: "modern", title: "Composition with Three Verticals", artist: "Viktor Steyn",
medium: "Oil on linen", dims: "130 × 97 cm", cat: "ALD.1912.01", credit: "Purchase, the Modern Fund, 1962",
note: "An early step toward abstraction: the figure is gone, the structure remains. Architecture of pure colour and edge.", plate: ["#b8842c", "#e6c879"] },
{ year: 1937, era: "modern", title: "Machine Pastoral", artist: "Inés Carrega",
medium: "Tempera and graphite on board", dims: "82 × 110 cm", cat: "ALD.1937.09", credit: "Gift of the artist's estate",
note: "Cogs and cornfields share one flattened plane — a wry interwar dialogue between industry and the land.", plate: ["#8a6a1c", "#d4b85a"] },
{ year: 1961, era: "modern", title: "Field No. 7 (Cadmium)", artist: "Roy Asher",
medium: "Acrylic on unprimed canvas", dims: "203 × 178 cm", cat: "ALD.1961.18", credit: "Purchase, 1979",
note: "Stained colour soaks the weave directly — scale and saturation become the whole subject of the work.", plate: ["#c46a2a", "#f0a85a"] },
{ year: 1978, era: "contemporary", title: "Inventory of a Borrowed Room", artist: "Dana Okafor",
medium: "Mixed media installation (documented)", dims: "Dimensions variable", cat: "ALD.1978.22", credit: "Commissioned by the gallery",
note: "A reconstructed domestic interior of found objects, catalogued like specimens. Memory presented as archive.", plate: ["#2f6a48", "#7fb893"] },
{ year: 1996, era: "contemporary", title: "Signal / Noise", artist: "Mara Lindqvist",
medium: "Single-channel video, 11 min", dims: "Edition 2 of 5", cat: "ALD.1996.31", credit: "Gift of the Lindqvist Foundation",
note: "Looped broadcast static resolving, almost, into a face. An early gallery acquisition of time-based media.", plate: ["#1c4a34", "#56a578"] },
{ year: 2019, era: "contemporary", title: "Carbon Garden", artist: "Joon-ho Bae",
medium: "Reclaimed steel and living moss", dims: "240 × 180 × 90 cm", cat: "ALD.2019.40", credit: "Purchase, the Sustainability Fund",
note: "A growing sculpture maintained by the conservation team — the collection's most recent and only living work.", plate: ["#3f7d56", "#a3d4b3"] }
];
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
var track = $("#track");
var bands = $("#bands");
var scrub = $("#scrub");
var scrubYear = $("#scrubYear");
var detail = $("#detail");
var count = $("#count");
var toastEl = $("#toast");
var activeEra = "all";
var openYear = null;
/* ---- toast ---- */
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("is-on"); }, 2200);
}
function plate(colors) {
return "linear-gradient(135deg, " + colors[0] + " 0%, " + colors[1] + " 100%)";
}
/* ---- build scrubber bands ---- */
function buildBands() {
var min = 1600, max = 2025, span = max - min;
BAND_ORDER.forEach(function (key) {
var e = ERAS[key];
var el = document.createElement("div");
el.className = "scrubber__band";
el.dataset.era = key;
el.style.flex = ((e.to - e.from) / span) + " 1 0";
el.style.background = e.color;
el.style.marginLeft = "0";
el.textContent = e.label;
bands.appendChild(el);
});
}
/* ---- build milestones ---- */
function buildTrack() {
track.innerHTML = "";
MILESTONES.forEach(function (m, i) {
var era = ERAS[m.era];
var li = document.createElement("li");
li.className = "milestone";
li.dataset.era = m.era;
li.dataset.year = String(m.year);
li.style.setProperty("--era", era.color);
li.innerHTML =
'<span class="milestone__marker"></span>' +
'<span class="milestone__year">' + m.year + '</span>' +
'<button class="milestone__card" type="button" aria-expanded="false">' +
'<span class="milestone__plate" style="background:' + plate(m.plate) + '"></span>' +
'<span class="milestone__text">' +
'<span class="milestone__badge">' + era.label + '</span>' +
'<span class="milestone__title">' + m.title + '</span>' +
'<span class="milestone__artist">' + m.artist + '</span>' +
'</span>' +
'</button>';
var btn = li.querySelector(".milestone__card");
btn.addEventListener("click", function () { openDetail(m, li, btn); });
track.appendChild(li);
});
updateCount();
}
/* ---- detail ---- */
function openDetail(m, li, btn) {
var era = ERAS[m.era];
$$(".milestone").forEach(function (n) {
n.classList.remove("is-open");
var b = n.querySelector(".milestone__card");
if (b) b.setAttribute("aria-expanded", "false");
});
li.classList.add("is-open");
btn.setAttribute("aria-expanded", "true");
openYear = m.year;
var card = $(".detail__card");
card.style.setProperty("--era", era.color);
$("#detailPlate").style.background = plate(m.plate);
$("#detailEra").textContent = era.label + " · " + era.from + "–" + era.to;
$("#detailYear").textContent = m.year;
$("#detailTitle").textContent = m.title;
$("#detailArtist").textContent = m.artist;
$("#detailNote").textContent = m.note;
var meta = $("#detailMeta");
meta.innerHTML = "";
var rows = [
["Medium", m.medium],
["Dimensions", m.dims],
["Catalogue", m.cat],
["Credit", m.credit]
];
rows.forEach(function (r) {
var dt = document.createElement("dt"); dt.textContent = r[0];
var dd = document.createElement("dd"); dd.textContent = r[1];
meta.appendChild(dt); meta.appendChild(dd);
});
detail.hidden = false;
// sync scrubber + center the card
scrub.value = String(m.year);
onScrub(true);
detail.scrollIntoView({ behavior: "smooth", block: "nearest" });
centerMilestone(li);
}
function closeDetail() {
detail.hidden = true;
openYear = null;
$$(".milestone").forEach(function (n) {
n.classList.remove("is-open");
var b = n.querySelector(".milestone__card");
if (b) b.setAttribute("aria-expanded", "false");
});
}
$("#detailClose").addEventListener("click", function () {
closeDetail();
toast("Label closed");
});
/* ---- era filter ---- */
function setEra(era) {
activeEra = era;
$$(".era-chip").forEach(function (c) {
var on = c.dataset.era === era;
c.classList.toggle("is-active", on);
c.setAttribute("aria-pressed", on ? "true" : "false");
});
$$(".scrubber__band").forEach(function (b) {
b.classList.toggle("is-dim", era !== "all" && b.dataset.era !== era);
});
$$(".milestone").forEach(function (n) {
n.classList.toggle("is-faded", era !== "all" && n.dataset.era !== era);
});
updateCount();
if (era !== "all") {
var first = $('.milestone[data-era="' + era + '"]');
if (first) centerMilestone(first);
var e = ERAS[era];
toast(e.label + " — " + e.from + " to " + e.to);
} else {
toast("Showing all five eras");
}
}
$$(".era-chip").forEach(function (chip) {
chip.addEventListener("click", function () { setEra(chip.dataset.era); });
});
/* ---- scrubber ---- */
function nearestMilestone(year) {
var best = MILESTONES[0], bestD = Infinity;
MILESTONES.forEach(function (m) {
if (activeEra !== "all" && m.era !== activeEra) return;
var d = Math.abs(m.year - year);
if (d < bestD) { bestD = d; best = m; }
});
return best;
}
function eraAt(year) {
for (var i = 0; i < BAND_ORDER.length; i++) {
var e = ERAS[BAND_ORDER[i]];
if (year >= e.from && year <= e.to) return BAND_ORDER[i];
}
return null;
}
function onScrub(silent) {
var year = parseInt(scrub.value, 10);
scrubYear.textContent = year;
var era = eraAt(year);
$$(".scrubber__band").forEach(function (b) {
b.classList.toggle("is-dim", !!era && b.dataset.era !== era);
});
if (!silent) {
var m = nearestMilestone(year);
var li = $('.milestone[data-year="' + m.year + '"]');
if (li) centerMilestone(li);
}
}
scrub.addEventListener("input", function () { onScrub(false); });
function centerMilestone(li) {
if (!li) return;
var isVertical = window.matchMedia("(max-width: 520px)").matches;
li.scrollIntoView({ behavior: "smooth", block: isVertical ? "center" : "nearest", inline: "center" });
}
function updateCount() {
var n = activeEra === "all"
? MILESTONES.length
: MILESTONES.filter(function (m) { return m.era === activeEra; }).length;
var label = activeEra === "all" ? "across five eras" : "in " + ERAS[activeEra].label;
count.textContent = n + " milestones " + label;
}
/* ---- keyboard: arrows step through visible milestones ---- */
function visibleMilestones() {
return MILESTONES.filter(function (m) {
return activeEra === "all" || m.era === activeEra;
});
}
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !detail.hidden) {
closeDetail();
return;
}
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
var tag = (e.target.tagName || "").toLowerCase();
if (tag === "input") return; // let the range slider handle its own arrows
var list = visibleMilestones();
if (!list.length) return;
var idx = 0;
if (openYear != null) {
for (var i = 0; i < list.length; i++) { if (list[i].year === openYear) { idx = i; break; } }
idx += (e.key === "ArrowRight" ? 1 : -1);
} else {
idx = e.key === "ArrowRight" ? 0 : list.length - 1;
}
idx = Math.max(0, Math.min(list.length - 1, idx));
var m = list[idx];
var li = $('.milestone[data-year="' + m.year + '"]');
var btn = li && li.querySelector(".milestone__card");
if (btn) { btn.focus(); openDetail(m, li, btn); }
e.preventDefault();
});
/* ---- init ---- */
buildBands();
buildTrack();
onScrub(true);
updateCount();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Museum — Historical Timeline</title>
<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=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="frame">
<header class="masthead">
<div class="masthead__brand">
<span class="sigil" aria-hidden="true">
<svg viewBox="0 0 40 40" width="40" height="40" role="img" aria-hidden="true">
<rect x="2" y="2" width="36" height="36" rx="3" fill="none" stroke="currentColor" stroke-width="1.2"/>
<path d="M8 30 L20 9 L32 30 Z" fill="none" stroke="currentColor" stroke-width="1.2"/>
<line x1="14" y1="30" x2="14" y2="20" stroke="currentColor" stroke-width="1.2"/>
<line x1="20" y1="30" x2="20" y2="16" stroke="currentColor" stroke-width="1.2"/>
<line x1="26" y1="30" x2="26" y2="20" stroke="currentColor" stroke-width="1.2"/>
</svg>
</span>
<div>
<p class="masthead__eyebrow">The Aldworth Collection</p>
<h1 class="masthead__title">Five Centuries of Form</h1>
</div>
</div>
<p class="masthead__lede">A walk through the movements that shaped the gallery's holdings — from the long shadow of the Baroque to the present day. Choose an era, scrub the years, and open any milestone to read its label.</p>
</header>
<nav class="eras" aria-label="Art movements">
<button class="era-chip is-active" data-era="all" aria-pressed="true">All eras</button>
<button class="era-chip" data-era="baroque" aria-pressed="false"><span class="era-chip__dot" data-era="baroque"></span>Baroque</button>
<button class="era-chip" data-era="romantic" aria-pressed="false"><span class="era-chip__dot" data-era="romantic"></span>Romanticism</button>
<button class="era-chip" data-era="impressionist" aria-pressed="false"><span class="era-chip__dot" data-era="impressionist"></span>Impressionism</button>
<button class="era-chip" data-era="modern" aria-pressed="false"><span class="era-chip__dot" data-era="modern"></span>Modernism</button>
<button class="era-chip" data-era="contemporary" aria-pressed="false"><span class="era-chip__dot" data-era="contemporary"></span>Contemporary</button>
</nav>
<section class="scrubber" aria-label="Timeline navigation">
<div class="scrubber__bands" id="bands" aria-hidden="true"></div>
<input
type="range"
id="scrub"
class="scrubber__range"
min="1600" max="2025" step="1" value="1600"
aria-label="Scrub through the years"
/>
<div class="scrubber__readout">
<span class="scrubber__year" id="scrubYear">1600</span>
<span class="scrubber__hint">Drag to travel through the years · use ← →</span>
</div>
</section>
<main class="timeline" id="timeline" tabindex="0" aria-label="Timeline of milestones">
<div class="timeline__rail" aria-hidden="true"></div>
<ol class="track" id="track"></ol>
</main>
<aside class="detail" id="detail" hidden aria-live="polite">
<div class="detail__card" role="region" aria-label="Milestone detail">
<button class="detail__close" id="detailClose" aria-label="Close detail">×</button>
<div class="detail__plate" id="detailPlate" aria-hidden="true"></div>
<div class="detail__body">
<p class="detail__era" id="detailEra"></p>
<p class="detail__year" id="detailYear"></p>
<h2 class="detail__title" id="detailTitle"></h2>
<p class="detail__artist" id="detailArtist"></p>
<dl class="detail__meta" id="detailMeta"></dl>
<p class="detail__note" id="detailNote"></p>
</div>
</div>
</aside>
<footer class="colophon">
<span class="colophon__count" id="count"></span>
<span class="colophon__disc">Illustrative UI · demo data only.</span>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Historical Timeline
A timeline for narrating a collection across time. Five art movements — Baroque, Romanticism, Impressionism, Modernism and Contemporary — are drawn as colour-coded era bands above a scrubber, with twelve fictional milestones threaded along a horizontal rail. Each milestone is a card with an inline plate, a year, a movement badge, and the title and artist of a work; the markers ride the rail in chronological order so the eye reads the centuries left to right.
Dragging the scrubber moves through the years, dims every band but the one you are inside, and glides the track to the nearest work. Era chips above filter the rail, fading out everything off-movement and recentring on the first piece. Clicking any card opens a typeset museum label below the rail — medium, dimensions, catalogue number, credit line and a short curatorial note — accented in the era’s colour. The left and right arrow keys step between visible works, Escape closes the label, and on narrow screens the whole rail folds into a vertical timeline.
Everything is vanilla HTML, CSS and JavaScript: refined Cormorant Garamond display type for titles, quiet Inter for the interface, CSS-gradient plates standing in for artworks, a small toast() helper, visible focus rings and a layout that holds together down to about 360px.
Illustrative UI only — demo data; not a real museum system.