Travel — "Top 10 Places" Listicle
A scannable, shareable travel listicle that ranks ten fictional beaches from one to ten. Each numbered entry leads with a full-bleed CSS gradient hero, an oversized rank badge, a vivid blurb, and a row of quick facts — best for, when to go, and a price tier — plus a mini CSS map chip with coordinates. A sticky ranked nav scroll-jumps between picks and scroll-spies the active one, an add-to-trip heart saves favourites, and a surprise-me button drops you on a random shore.
MCP
Code
/* ============================================================
Travel — "Top 10 Places" Listicle
============================================================ */
:root {
--bg: #fbf7f1;
--paper: #fffdf9;
--ink: #241f1a;
--muted: #6b6259;
--teal: #1f8a8a;
--teal-deep: #156a6a;
--coral: #e8623f;
--coral-deep: #c44a2a;
--sand: #e7d8c3;
--gold: #d99a2b;
--line: rgba(36, 31, 26, 0.12);
--line-strong: rgba(36, 31, 26, 0.22);
--shadow: 0 18px 44px -22px rgba(36, 31, 26, 0.45);
--shadow-sm: 0 6px 18px -12px rgba(36, 31, 26, 0.5);
--radius: 18px;
--serif: "Fraunces", Georgia, "Times New Roman", serif;
--sans: "Work Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
* { animation: none !important; transition: none !important; }
}
img { max-width: 100%; }
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--ink);
color: var(--paper);
padding: 0.6rem 1rem;
border-radius: 0 0 10px 0;
z-index: 50;
font-weight: 600;
}
.skip-link:focus { left: 0; }
:focus-visible {
outline: 3px solid var(--teal);
outline-offset: 3px;
border-radius: 6px;
}
/* ---------- Buttons ---------- */
.btn {
font-family: var(--sans);
font-size: 0.86rem;
font-weight: 600;
border: 1px solid var(--line-strong);
background: var(--paper);
color: var(--ink);
padding: 0.5rem 0.9rem;
border-radius: 999px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.4rem;
transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease,
box-shadow 0.14s ease;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
.btn:active { transform: translateY(0); }
.btn--ghost {
background: transparent;
border-color: var(--line);
}
.btn--ghost:hover { background: rgba(31, 138, 138, 0.1); border-color: var(--teal); }
.btn--solid {
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
}
.btn--solid:hover { background: var(--teal-deep); border-color: var(--teal-deep); }
.btn__ico { font-size: 1rem; line-height: 1; }
/* ---------- Masthead ---------- */
.masthead {
position: relative;
padding: clamp(2.4rem, 6vw, 4.5rem) 1.25rem clamp(2rem, 5vw, 3.5rem);
overflow: hidden;
color: #fdf6ec;
background:
radial-gradient(120% 90% at 12% 0%, rgba(255, 214, 153, 0.35), transparent 55%),
radial-gradient(130% 120% at 100% 100%, rgba(31, 138, 138, 0.55), transparent 60%),
linear-gradient(160deg, #163b46 0%, #1f6464 48%, #2a8a7d 100%);
}
.masthead::after {
/* horizon sun-glint */
content: "";
position: absolute;
right: -8%;
top: 8%;
width: 240px;
height: 240px;
border-radius: 50%;
background: radial-gradient(circle at 50% 50%, rgba(255, 224, 168, 0.85), rgba(232, 98, 63, 0.25) 60%, transparent 70%);
filter: blur(2px);
pointer-events: none;
}
.masthead__inner {
max-width: 1080px;
margin: 0 auto;
position: relative;
z-index: 1;
}
.kicker {
margin: 0 0 0.9rem;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #ffd591;
}
.masthead__title {
margin: 0;
font-family: var(--serif);
font-weight: 900;
font-size: clamp(2.5rem, 8vw, 5.2rem);
line-height: 0.98;
letter-spacing: -0.01em;
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.25);
}
.masthead__lede {
margin: 1.1rem 0 1.6rem;
max-width: 38ch;
font-size: clamp(1rem, 2.4vw, 1.2rem);
color: rgba(253, 246, 236, 0.92);
}
.masthead__meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.7rem;
font-size: 0.86rem;
color: rgba(253, 246, 236, 0.85);
}
.masthead__meta .byline { font-weight: 600; color: #fff; }
.masthead__meta .dot { opacity: 0.6; }
.masthead .btn--ghost {
color: #fff;
border-color: rgba(255, 255, 255, 0.4);
}
.masthead .btn--ghost:hover {
background: rgba(255, 255, 255, 0.16);
border-color: #fff;
}
/* ---------- Layout ---------- */
.layout {
max-width: 1080px;
margin: 0 auto;
padding: clamp(1.6rem, 4vw, 3rem) 1.25rem;
display: grid;
grid-template-columns: 230px minmax(0, 1fr);
gap: clamp(1.5rem, 4vw, 3rem);
align-items: start;
}
/* ---------- Sticky ranked nav ---------- */
.ranknav {
position: sticky;
top: 1.25rem;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1.1rem 1rem 1.2rem;
box-shadow: var(--shadow-sm);
}
.ranknav__title {
margin: 0 0 0.7rem;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.ranknav__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
counter-reset: rank;
}
.ranknav__link {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
text-align: left;
background: none;
border: 1px solid transparent;
border-radius: 11px;
padding: 0.4rem 0.5rem;
cursor: pointer;
font-family: var(--sans);
font-size: 0.82rem;
color: var(--ink);
transition: background 0.14s ease, border-color 0.14s ease;
}
.ranknav__link:hover { background: rgba(31, 138, 138, 0.09); }
.ranknav__num {
flex: none;
width: 1.7rem;
height: 1.7rem;
display: grid;
place-items: center;
border-radius: 8px;
background: var(--sand);
color: var(--ink);
font-family: var(--serif);
font-weight: 700;
font-size: 0.9rem;
transition: background 0.14s ease, color 0.14s ease;
}
.ranknav__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.ranknav__link.is-active {
background: rgba(31, 138, 138, 0.12);
border-color: rgba(31, 138, 138, 0.3);
}
.ranknav__link.is-active .ranknav__num {
background: var(--teal);
color: #fff;
}
.ranknav__link.is-active .ranknav__label { font-weight: 700; }
.ranknav__progress {
margin-top: 1rem;
height: 5px;
border-radius: 999px;
background: var(--sand);
overflow: hidden;
}
.ranknav__bar {
display: block;
height: 100%;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--teal), var(--coral));
transition: width 0.18s ease;
}
/* ---------- Entries ---------- */
.list {
display: flex;
flex-direction: column;
gap: clamp(2.4rem, 5vw, 3.5rem);
outline: none;
}
.entry {
scroll-margin-top: 1.25rem;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: box-shadow 0.25s ease, transform 0.25s ease;
}
.entry.is-active { box-shadow: 0 24px 56px -24px rgba(31, 138, 138, 0.55); }
.entry__hero {
position: relative;
height: clamp(180px, 30vw, 280px);
overflow: hidden;
}
.entry__rank {
position: absolute;
left: clamp(0.8rem, 3vw, 1.6rem);
bottom: -0.4rem;
z-index: 2;
font-family: var(--serif);
font-weight: 900;
font-size: clamp(4.2rem, 13vw, 8.5rem);
line-height: 0.8;
color: #fffdf9;
-webkit-text-stroke: 2px rgba(36, 31, 26, 0.35);
text-shadow: 0 6px 20px rgba(36, 31, 26, 0.45);
pointer-events: none;
}
.entry__top {
position: absolute;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding: 0.9rem;
gap: 0.5rem;
z-index: 2;
}
.entry__badge {
background: rgba(255, 253, 249, 0.92);
color: var(--ink);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 0.32rem 0.7rem;
border-radius: 999px;
box-shadow: var(--shadow-sm);
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.entry__badge--gold { color: var(--coral-deep); }
.entry__save {
flex: none;
width: 2.3rem;
height: 2.3rem;
border-radius: 999px;
border: none;
background: rgba(255, 253, 249, 0.92);
cursor: pointer;
font-size: 1.1rem;
line-height: 1;
display: grid;
place-items: center;
box-shadow: var(--shadow-sm);
transition: transform 0.14s ease, background 0.14s ease;
}
.entry__save:hover { transform: scale(1.08); }
.entry__save[aria-pressed="true"] { background: var(--coral); }
.entry__body {
padding: clamp(1.2rem, 3vw, 1.8rem) clamp(1.2rem, 3vw, 1.8rem) clamp(1.4rem, 3vw, 1.9rem);
}
.entry__eyebrow {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--teal);
}
.entry__region {
color: var(--muted);
text-transform: none;
letter-spacing: 0;
font-weight: 500;
}
.entry__name {
margin: 0.4rem 0 0.2rem;
font-family: var(--serif);
font-weight: 700;
font-size: clamp(1.5rem, 4vw, 2.1rem);
line-height: 1.08;
}
.entry__rating {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.86rem;
color: var(--muted);
margin-bottom: 0.6rem;
}
.entry__stars { color: var(--gold); letter-spacing: 0.06em; }
.entry__blurb {
margin: 0 0 1.1rem;
font-size: clamp(0.96rem, 2.3vw, 1.05rem);
color: var(--ink);
max-width: 62ch;
}
/* Quick facts */
.facts {
list-style: none;
margin: 0 0 1.2rem;
padding: 1rem 1.1rem;
border: 1px dashed var(--line-strong);
border-radius: 14px;
background: rgba(231, 216, 195, 0.28);
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.9rem 1.1rem;
}
.facts li { min-width: 0; }
.facts dt,
.facts__k {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.2rem;
}
.facts__v {
font-size: 0.94rem;
font-weight: 600;
color: var(--ink);
}
.price { letter-spacing: 0.12em; }
.price__on { color: var(--coral); }
.price__off { color: var(--line-strong); }
.entry__foot {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.6rem;
}
/* Map link / mini map */
.entry__map {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 0.5rem 0.9rem 0.5rem 0.55rem;
border-radius: 999px;
border: 1px solid var(--line-strong);
background: var(--paper);
cursor: pointer;
font-family: var(--sans);
font-weight: 600;
font-size: 0.84rem;
color: var(--ink);
transition: border-color 0.14s ease, transform 0.14s ease;
}
.entry__map:hover { border-color: var(--teal); transform: translateY(-1px); }
.minimap {
flex: none;
width: 2.1rem;
height: 2.1rem;
border-radius: 8px;
border: 1px solid var(--line);
background:
linear-gradient(135deg, #cfe9e0, #a9d6cf 60%, #f1e2c8);
position: relative;
overflow: hidden;
}
.minimap::before {
/* coastline */
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 80% 70%, var(--teal) 0 22%, transparent 23%),
linear-gradient(120deg, transparent 45%, rgba(255, 253, 249, 0.7) 46% 54%, transparent 55%);
}
.minimap::after {
/* pin */
content: "";
position: absolute;
left: 50%;
top: 38%;
width: 7px;
height: 7px;
border-radius: 50% 50% 50% 0;
transform: translate(-50%, -50%) rotate(-45deg);
background: var(--coral);
box-shadow: 0 0 0 2px rgba(255, 253, 249, 0.9);
}
.entry__coords { color: var(--muted); font-weight: 500; }
/* ---------- Footer ---------- */
.footer {
max-width: 1080px;
margin: 0 auto;
padding: 2rem 1.25rem 3rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 0.86rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 1.4rem;
transform: translate(-50%, 1.5rem);
background: var(--ink);
color: var(--paper);
padding: 0.7rem 1.1rem;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 600;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 60;
max-width: calc(100vw - 2rem);
text-align: center;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* flash highlight when jumping to an entry */
@keyframes entry-flash {
0% { box-shadow: 0 0 0 0 rgba(232, 98, 63, 0.55); }
100% { box-shadow: 0 24px 56px -24px rgba(31, 138, 138, 0.55); }
}
.entry.flash { animation: entry-flash 1s ease; }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
}
.ranknav {
position: static;
}
.ranknav__list {
flex-direction: row;
flex-wrap: wrap;
gap: 0.4rem;
}
.ranknav__link {
width: auto;
padding: 0.35rem 0.55rem 0.35rem 0.35rem;
}
.ranknav__label { display: none; }
.ranknav__progress { display: none; }
}
@media (max-width: 540px) {
.facts {
grid-template-columns: 1fr 1fr;
}
.entry__foot { width: 100%; }
.entry__foot .btn,
.entry__map { width: 100%; justify-content: center; }
}
@media (max-width: 380px) {
.facts { grid-template-columns: 1fr; }
}/* ============================================================
Travel — "Top 10 Places" Listicle
Vanilla JS: ranked nav, scroll-spy, surprise-me, save, share.
============================================================ */
(function () {
"use strict";
/* ---- Data: 10 fictional beaches, ranked 1 = best ---- */
var BEACHES = [
{
id: "luma-cove",
name: "Luma Cove",
region: "Isla Verema, South Pacific",
tag: "Editor's pick",
best: "Snorkelling",
when: "May–Sep",
cost: 4,
rating: 4.9,
coords: "12.4°S · 168.1°W",
blurb:
"A horseshoe of pale gold sand wrapped around water so clear the reef looks painted on. Spinner dolphins patrol the mouth of the cove at dawn, and a single driftwood shack serves grilled mango and coconut espresso.",
grad:
"radial-gradient(120% 90% at 20% 10%, #fff3d6, transparent 50%), linear-gradient(170deg,#7fd4d0 0%,#39a7b0 42%,#1f6e7a 100%)",
},
{
id: "ferrowind",
name: "Ferrowind Strand",
region: "Nordskär, North Atlantic",
tag: "Dramatic coast",
best: "Storm-watching",
when: "Oct–Feb",
cost: 2,
rating: 4.7,
coords: "64.9°N · 23.6°W",
blurb:
"Black volcanic sand meets white surf under skies that change five times an hour. Bring a flask — the wind here has opinions — and stay for basalt sea-stacks that glow rust-red at the late northern sunset.",
grad:
"linear-gradient(165deg,#2b3a4a 0%,#41617a 45%,#9fb4bf 100%), radial-gradient(80% 60% at 70% 20%, rgba(232,98,63,.4), transparent 60%)",
},
{
id: "palmoro",
name: "Palmoro Bay",
region: "Costa Dorella, Mediterranean",
tag: "Family favourite",
best: "Families",
when: "Jun–Aug",
cost: 3,
rating: 4.8,
coords: "39.1°N · 3.2°E",
blurb:
"Shallow turquoise shelves run a hundred metres out, perfect for small swimmers, while a pine-shaded promenade keeps the gelato cold. Sunset paddleboard tours leave from the old stone jetty nightly.",
grad:
"radial-gradient(110% 80% at 15% 5%, #fff0c6, transparent 45%), linear-gradient(175deg,#86dcd0 0%,#4fb6c4 50%,#2f7fa6 100%)",
},
{
id: "saffron-mile",
name: "Saffron Mile",
region: "Maravelle, Indian Ocean",
tag: "Sunset legend",
best: "Sunsets",
when: "Nov–Apr",
cost: 5,
rating: 4.9,
coords: "4.2°S · 73.4°E",
blurb:
"A mile of rose-tinted sand that turns molten amber at golden hour, fringed by overwater bungalows on stilts. Bioluminescent plankton light the shallows after dark — like swimming through quiet fireworks.",
grad:
"linear-gradient(175deg,#ffb27a 0%,#e8623f 35%,#a93f6a 70%,#5a3a78 100%)",
},
{
id: "kelp-harbor",
name: "Kelp Harbour",
region: "Pacific Reach, California-North",
tag: "Wildlife",
best: "Sea otters",
when: "Apr–Oct",
cost: 2,
rating: 4.6,
coords: "36.6°N · 121.9°W",
blurb:
"Tide pools brim with anemones and the kelp forest just offshore hides a raft of dozing otters. A cliff-top trail links three coves; pack a thermos and you can beach-hop the whole afternoon.",
grad:
"linear-gradient(170deg,#2f6f63 0%,#4f9a86 45%,#bcd9c6 100%), radial-gradient(70% 50% at 80% 15%, rgba(255,232,168,.5), transparent 55%)",
},
{
id: "azulita",
name: "Azulita Lagoon",
region: "Caya Blanca, Caribbean",
tag: "Calm water",
best: "Swimming",
when: "Dec–May",
cost: 4,
rating: 4.8,
coords: "18.3°N · 78.0°W",
blurb:
"A reef-protected lagoon the colour of a swimming-pool advert, with sand so fine it squeaks. Hammocks strung between sea-grape trees, a floating taco bar, and zero waves bigger than a ripple.",
grad:
"radial-gradient(100% 80% at 20% 10%, #fffbe4, transparent 40%), linear-gradient(175deg,#9ef0e4 0%,#46c7d6 45%,#2790c0 100%)",
},
{
id: "dune-songs",
name: "Dune Songs",
region: "Erg Soleil, Atlantic Sahara",
tag: "Surreal",
best: "Photography",
when: "Sep–Mar",
cost: 3,
rating: 4.5,
coords: "23.7°N · 15.9°W",
blurb:
"Where amber dunes pour straight into a cold green ocean — surfers and camels share the same beach. The sand hums when the trade winds rise, and night skies here are a planetarium with no roof.",
grad:
"linear-gradient(170deg,#f2c879 0%,#e3a24f 30%,#2f8f9a 68%,#1d5e6e 100%)",
},
{
id: "glasswater",
name: "Glasswater Point",
region: "Tairoa, South Island",
tag: "Surf",
best: "Surfing",
when: "Mar–Jun",
cost: 2,
rating: 4.7,
coords: "45.9°S · 170.6°E",
blurb:
"A right-hand point break that peels for two hundred metres on a good swell, with a steaming espresso van parked above the cliff. Penguins waddle ashore at dusk while the last surfers paddle in.",
grad:
"linear-gradient(170deg,#1f4f63 0%,#2d7d8c 45%,#7fc3c9 100%), radial-gradient(60% 50% at 75% 20%, rgba(255,243,214,.4), transparent 55%)",
},
{
id: "rosé-reef",
name: "Rosé Reef",
region: "Sanguine Isles, Tasman Sea",
tag: "Pink sand",
best: "Romance",
when: "Oct–Mar",
cost: 5,
rating: 4.8,
coords: "29.0°S · 159.9°E",
blurb:
"Crushed coral tints this sand a soft blush that deepens at low tide. A single barefoot restaurant cooks the day's catch over driftwood, and the reef just beyond the break is a riot of clownfish.",
grad:
"radial-gradient(110% 80% at 20% 10%, #fff2ec, transparent 45%), linear-gradient(175deg,#f6c2c8 0%,#e88a98 40%,#5fb6bf 100%)",
},
{
id: "lantern-bay",
name: "Lantern Bay",
region: "Hồ Quế, Andaman Coast",
tag: "Nightlife",
best: "Beach bars",
when: "Nov–Apr",
cost: 3,
rating: 4.6,
coords: "8.0°N · 98.3°E",
blurb:
"By day, longtail boats ferry you to limestone islands; by night, paper lanterns float over warm water and fire-dancers spin along the tideline. Crisp, cold and barefoot — the bay that never quite sleeps.",
grad:
"linear-gradient(175deg,#2a2150 0%,#5a3a78 35%,#d9772f 75%,#ffce7a 100%)",
},
];
/* ---- Helpers ---- */
var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); };
var listEl = $("#list");
var navListEl = $("#ranknav-list");
var progressBar = $("#progress-bar");
var toastEl = $("#toast");
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function priceTier(n) {
var on = "", off = "";
for (var i = 0; i < n; i++) on += "$";
for (var j = 0; j < 5 - n; j++) off += "$";
return (
'<span class="price"><span class="price__on">' +
on +
'</span><span class="price__off">' +
off +
"</span></span>"
);
}
function stars(rating) {
var full = Math.round(rating);
var s = "";
for (var i = 0; i < 5; i++) s += i < full ? "★" : "☆";
return s;
}
function esc(str) {
return String(str).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
/* ---- Render entries + nav ---- */
var entryEls = [];
BEACHES.forEach(function (b, i) {
var rank = i + 1;
var entryId = "beach-" + rank;
var article = document.createElement("article");
article.className = "entry";
article.id = entryId;
article.dataset.rank = String(rank);
article.setAttribute("aria-labelledby", entryId + "-name");
article.innerHTML =
'<div class="entry__hero" style="background:' + b.grad + ';">' +
'<span class="entry__rank">' + rank + "</span>" +
'<div class="entry__top">' +
'<button class="entry__save" type="button" aria-pressed="false" ' +
'aria-label="Add ' + esc(b.name) + ' to your trip" title="Add to trip">🤍</button>' +
"</div>" +
"</div>" +
'<div class="entry__body">' +
'<p class="entry__eyebrow">' + esc(b.tag) +
' <span class="entry__region">· ' + esc(b.region) + "</span></p>" +
'<h2 class="entry__name" id="' + entryId + '-name">' + esc(b.name) + "</h2>" +
'<p class="entry__rating"><span class="entry__stars" aria-hidden="true">' +
stars(b.rating) + "</span> " +
'<span>' + b.rating.toFixed(1) + " · traveller score</span></p>" +
'<p class="entry__blurb">' + esc(b.blurb) + "</p>" +
'<ul class="facts">' +
'<li><div class="facts__k">Best for</div><div class="facts__v">' + esc(b.best) + "</div></li>" +
'<li><div class="facts__k">When to go</div><div class="facts__v">' + esc(b.when) + "</div></li>" +
'<li><div class="facts__k">Price tier</div><div class="facts__v">' + priceTier(b.cost) + "</div></li>" +
"</ul>" +
'<div class="entry__foot">' +
'<button class="entry__map" type="button" data-map="' + esc(b.name) + '">' +
'<span class="minimap" aria-hidden="true"></span>' +
"<span>View on map</span>" +
'<span class="entry__coords">' + esc(b.coords) + "</span>" +
"</button>" +
'<button class="btn btn--ghost" type="button" data-next="' + rank + '">Next ↓</button>' +
"</div>" +
"</div>";
listEl.appendChild(article);
entryEls.push(article);
// nav item
var li = document.createElement("li");
var link = document.createElement("button");
link.type = "button";
link.className = "ranknav__link";
link.dataset.target = entryId;
link.innerHTML =
'<span class="ranknav__num">' + rank + "</span>" +
'<span class="ranknav__label">' + esc(b.name) + "</span>";
li.appendChild(link);
navListEl.appendChild(li);
});
var navLinks = Array.prototype.slice.call(navListEl.querySelectorAll(".ranknav__link"));
/* ---- Active-state syncing ---- */
function setActive(rank) {
navLinks.forEach(function (l) {
var on = l.dataset.target === "beach-" + rank;
l.classList.toggle("is-active", on);
if (on) l.setAttribute("aria-current", "true");
else l.removeAttribute("aria-current");
});
entryEls.forEach(function (e) {
e.classList.toggle("is-active", e.dataset.rank === String(rank));
});
if (progressBar) {
progressBar.style.width = (rank / BEACHES.length) * 100 + "%";
}
}
function jumpTo(rank, flash) {
var el = document.getElementById("beach-" + rank);
if (!el) return;
el.scrollIntoView({ behavior: "smooth", block: "start" });
setActive(rank);
if (flash) {
el.classList.remove("flash");
void el.offsetWidth; // restart animation
el.classList.add("flash");
}
}
/* ---- Scroll-spy via IntersectionObserver ---- */
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(
function (entries) {
// choose the entry closest to the top that is intersecting
var best = null;
entries.forEach(function (en) {
if (en.isIntersecting) {
if (!best || en.boundingClientRect.top < best.boundingClientRect.top) {
best = en;
}
}
});
if (best) setActive(parseInt(best.target.dataset.rank, 10));
},
{ rootMargin: "-25% 0px -55% 0px", threshold: 0 }
);
entryEls.forEach(function (e) { io.observe(e); });
}
/* ---- Click handlers ---- */
navListEl.addEventListener("click", function (e) {
var link = e.target.closest(".ranknav__link");
if (!link) return;
var rank = parseInt(link.dataset.target.replace("beach-", ""), 10);
jumpTo(rank, true);
});
// keyboard arrows within the nav
navListEl.addEventListener("keydown", function (e) {
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
var idx = navLinks.indexOf(document.activeElement);
if (idx === -1) return;
e.preventDefault();
var next = e.key === "ArrowDown" ? idx + 1 : idx - 1;
if (next < 0) next = navLinks.length - 1;
if (next >= navLinks.length) next = 0;
navLinks[next].focus();
});
listEl.addEventListener("click", function (e) {
var save = e.target.closest(".entry__save");
if (save) {
var pressed = save.getAttribute("aria-pressed") === "true";
save.setAttribute("aria-pressed", String(!pressed));
save.textContent = pressed ? "🤍" : "❤️";
var name = save.getAttribute("aria-label").replace(/^Add | to your trip$/g, "");
toast(pressed ? "Removed " + name + " from your trip" : "Saved " + name + " to your trip");
return;
}
var mapBtn = e.target.closest("[data-map]");
if (mapBtn) {
toast("📍 Opening map for " + mapBtn.dataset.map);
return;
}
var nextBtn = e.target.closest("[data-next]");
if (nextBtn) {
var cur = parseInt(nextBtn.dataset.next, 10);
var nxt = cur >= BEACHES.length ? 1 : cur + 1;
jumpTo(nxt, true);
return;
}
});
/* ---- Masthead / footer actions ---- */
document.body.addEventListener("click", function (e) {
var btn = e.target.closest("[data-action]");
if (!btn) return;
var action = btn.dataset.action;
if (action === "surprise") {
var rank = Math.floor(Math.random() * BEACHES.length) + 1;
jumpTo(rank, true);
toast("🎲 You landed on #" + rank + " — " + BEACHES[rank - 1].name);
} else if (action === "top") {
window.scrollTo({ top: 0, behavior: "smooth" });
setActive(1);
} else if (action === "share") {
var shareData = {
title: "Top 10 Beaches on Earth",
text: "The 10 shorelines worth crossing an ocean for.",
url: location.href,
};
if (navigator.share) {
navigator.share(shareData).catch(function () {});
} else if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(location.href)
.then(function () { toast("🔗 Link copied to clipboard"); })
.catch(function () { toast("Share: " + location.href); });
} else {
toast("Share: " + location.href);
}
}
});
// initialise
setActive(1);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Top 10 Beaches on Earth — Wander Listicle</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=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700;9..144,900&family=Work+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#list">Skip to the ranking</a>
<header class="masthead" role="banner">
<div class="masthead__inner">
<p class="kicker">Wander Quarterly · The Coast Issue</p>
<h1 class="masthead__title">The Top 10 Beaches<br />on Earth</h1>
<p class="masthead__lede">
Ten shorelines worth crossing an ocean for — ranked, mapped, and ready
for your next trip. Fictional finds, real wanderlust.
</p>
<div class="masthead__meta">
<span class="byline">By the Wander field team</span>
<span class="dot" aria-hidden="true">·</span>
<span>12 min read</span>
<span class="dot" aria-hidden="true">·</span>
<button class="btn btn--ghost" type="button" data-action="surprise">
<span class="btn__ico" aria-hidden="true">🎲</span> Surprise me
</button>
<button class="btn btn--ghost" type="button" data-action="share">
<span class="btn__ico" aria-hidden="true">🔗</span> Share
</button>
</div>
</div>
</header>
<div class="layout">
<!-- Sticky ranked nav -->
<nav class="ranknav" aria-label="Jump to a ranked beach">
<p class="ranknav__title">The ranking</p>
<ol class="ranknav__list" id="ranknav-list"></ol>
<div class="ranknav__progress" aria-hidden="true">
<span class="ranknav__bar" id="progress-bar"></span>
</div>
</nav>
<main class="list" id="list" tabindex="-1">
<!-- entries injected by script.js -->
</main>
</div>
<footer class="footer" role="contentinfo">
<p>Wander Quarterly · Illustrative travel UI — fictional destinations, prices, and maps.</p>
<button class="btn btn--ghost" type="button" data-action="top">↑ Back to the top</button>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>“Top 10 Places” Listicle
A magazine-style countdown of the ten best beaches on Earth, built for skimming. Each entry opens with a numbered, full-bleed hero painted entirely in layered CSS gradients — sunset ambers, volcanic blacks, lagoon turquoises — overprinted with an oversized serif rank that bleeds off the corner. Below it sit a one-line eyebrow, a traveller score in stars, a punchy blurb, and a dashed “quick facts” panel covering what each shore is best for, the best months to visit, and a five-dollar price tier.
A sticky ranked nav on the left lists all ten picks one through ten. Clicking a rank smooth-scrolls to that entry, flashes it, and marks it active; an IntersectionObserver scroll-spy keeps the nav and a thin gradient progress bar in sync as you read, and arrow keys move focus between ranks. Every entry has an add-to-trip heart that toggles and toasts, a mini CSS map chip with fictional coordinates, and a “next” button to walk the countdown.
In the masthead, a Surprise me button jumps you to a random beach and announces which one, while Share uses the Web Share API where available and falls back to copying the link. The three-column facts grid collapses to two then one, the sticky sidebar becomes a horizontal chip row on narrow screens, and the whole layout stays readable down to 360px.
Illustrative travel UI only — fictional destinations, prices, and maps.