Keyboard-accessible tabs
NavigationPreloaded panels with one tab stop and predictable arrow-key navigation.
HTML + CSS + JavaScript
role="tablist" Navigation · Complete runnable example
Select existing document views from a short, explicit route list.
This preview runs in an isolated document. Clipboard writes and saved preferences may be blocked, and printing requires the standalone file. A dialog affects only the example. Reset reloads the original example state.
Use this example to explore route state in a small static interface. It switches between pre-existing sections and supports direct fragment URLs without requiring server route configuration.
The preview, copied text, and downloaded file use this same document.
<!doctype html>
<!--
MIT License
Copyright (c) 2026 MRHTML
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; form-action 'none'; object-src 'none'; base-uri 'none'"
/>
<title>MRHTML | Three views. One document.</title>
<style>
:root {
font-family: Arial, Helvetica, sans-serif;
color-scheme: light;
--ink: #192f34;
--muted: #56666a;
--paper: #f6f5f0;
--line: #cdd5d0;
--accent: #186354;
--soft: #e7efe5;
--mono: Consolas, 'Liberation Mono', monospace;
}
* {
box-sizing: border-box;
}
html {
scroll-padding-top: 90px;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
line-height: 1.6;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1,
h2,
h3 {
font-weight: 500;
letter-spacing: -0.045em;
line-height: 1.1;
}
h1 {
font-size: clamp(38px, 5.5vw, 65px);
}
h2 {
font-size: 29px;
}
h3 {
font-size: 21px;
}
p {
overflow-wrap: anywhere;
}
a {
color: var(--accent);
text-underline-offset: 4px;
}
button,
select {
font: inherit;
color: inherit;
}
button {
cursor: pointer;
}
button,
a,
summary {
touch-action: manipulation;
}
button:focus-visible,
a:focus-visible,
select:focus-visible,
[tabindex]:focus-visible,
summary:focus-visible {
outline: 3px solid #a94823;
outline-offset: 4px;
}
[hidden] {
display: none !important;
}
.wrap {
width: min(1050px, calc(100% - 64px));
margin-inline: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
padding: 28px 0;
border-bottom: 1px solid var(--line);
}
.brand {
font-size: 15px;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: -0.5px;
}
.mark {
display: grid;
place-items: center;
width: 28px;
height: 28px;
background: var(--accent);
color: #fff;
font-weight: bold;
font-size: 18px;
}
.header > p,
.eyebrow,
.small {
font: 10px/1.7 var(--mono);
letter-spacing: 0.03em;
}
.header > p {
color: var(--muted);
}
main {
padding-block: 43px 50px;
}
.intro {
margin-bottom: 30px;
max-width: 720px;
}
.eyebrow {
color: var(--accent);
margin-bottom: 13px;
}
.intro > p:last-child {
font-size: 15px;
max-width: 570px;
color: var(--muted);
margin-top: 18px;
}
.surface {
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 12px 32px #263e3010;
}
.button {
border: 1px solid var(--accent);
background: var(--accent);
color: #fff;
padding: 10px 16px;
min-height: 43px;
border-radius: 7px;
font-size: 13px;
font-weight: 600;
}
.button.secondary {
background: #fff;
color: var(--accent);
border-color: #a9bbb0;
}
.button[aria-disabled='true'],
button:disabled {
background: #edf0e9;
color: #5d695f;
border-color: #c9d1c4;
cursor: default;
}
.status {
font-size: 12px;
color: var(--muted);
min-height: 22px;
}
.note {
font-size: 12px;
color: var(--muted);
margin-top: 20px;
max-width: 75ch;
}
.footer {
display: flex;
justify-content: space-between;
gap: 20px;
padding-block: 22px;
border-top: 1px solid var(--line);
font-size: 10px;
color: var(--muted);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
noscript p {
padding: 15px;
background: #e8ece2;
border-left: 3px solid var(--accent);
font-size: 13px;
margin-bottom: 20px;
}
code,
kbd {
font: 12px var(--mono);
}
kbd {
border: 1px solid #bcc7bd;
border-radius: 4px;
padding: 2px 5px;
background: #fff;
}
button:disabled:focus {
outline: none;
}
@media (max-width: 600px) {
.wrap {
width: calc(100% - 36px);
}
.header {
padding-block: 22px;
}
.header > p {
font-size: 8px;
max-width: 105px;
text-align: right;
}
.brand {
font-size: 13px;
gap: 8px;
}
main {
padding-block: 30px 35px;
}
.intro {
margin-bottom: 26px;
}
.intro > p:last-child {
font-size: 14px;
}
.eyebrow {
font-size: 9px;
}
h1 {
font-size: 40px;
}
.footer {
flex-wrap: wrap;
font-size: 9px;
}
}
@media (max-width: 350px) {
h1 {
font-size: 35px;
}
.button {
padding: 10px 12px;
font-size: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
}
}
.notebook {
overflow: hidden;
}
.route-nav {
display: flex;
gap: 5px;
padding: 15px 20px;
border-bottom: 1px solid var(--line);
background: #e9eee3;
}
.route-nav a {
padding: 12px 19px;
border-radius: 6px;
text-decoration: none;
font-size: 13px;
color: #46623e;
}
.route-nav a[aria-current='page'] {
background: #fff;
color: #1b422e;
box-shadow: 0 2px 8px #263b2210;
font-weight: bold;
}
.route-view {
padding: 42px;
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 42px;
align-items: center;
min-height: 385px;
}
.route-view + .route-view {
border-top: 1px solid var(--line);
}
.route-view h2 {
font-size: 43px;
line-height: 1.05;
}
.route-view p:not(.eyebrow) {
font-size: 14px;
color: var(--muted);
margin-top: 20px;
max-width: 42ch;
}
.route-link {
display: inline-block;
margin-top: 23px;
font-size: 13px;
}
.notebook-art {
height: 275px;
background: #e6dfc8;
position: relative;
border-radius: 7px;
display: grid;
place-items: center;
overflow: hidden;
}
.notebook-art i {
position: absolute;
width: 165px;
height: 220px;
transform: rotate(13deg);
background: #abb985;
border: 1px solid #7d8f5d;
}
.notebook-art i:nth-child(2) {
transform: rotate(-9deg);
background: #f5f5e9;
left: 50px;
}
.notebook-art > span {
position: relative;
font-size: 51px;
line-height: 0.92;
letter-spacing: -3px;
transform: rotate(-9deg);
color: #3b5b2f;
}
.route-status {
margin-top: 20px;
}
.route-view ul {
padding-left: 20px;
font-size: 13px;
color: var(--muted);
line-height: 2;
margin-top: 20px;
}
.workbench-note {
background: #e4ecdc;
padding: 27px;
min-height: 260px;
border-top: 4px solid #708756;
}
.workbench-note p {
font-size: 30px !important;
line-height: 1.25;
letter-spacing: -1px;
color: #36562e !important;
}
.workbench-note strong {
font-weight: 500;
}
.notes-art {
background: #f0e2c5;
padding: 30px;
min-height: 265px;
display: flex;
flex-direction: column;
gap: 19px;
transform: rotate(2deg);
}
.notes-art span:first-child {
font: 40px var(--mono);
color: #775727;
}
.notes-art i {
height: 1px;
background: #ae946b;
}
.notes-art span:last-child {
font-size: 12px;
color: #775727;
margin-top: 8px;
}
.not-found {
grid-template-columns: 1fr;
}
.not-found #unknown-route {
font-family: var(--mono);
font-size: 12px;
overflow-wrap: anywhere;
}
@media (max-width: 640px) {
.route-nav {
padding: 12px;
gap: 3px;
justify-content: space-between;
}
.route-nav a {
padding: 11px 12px;
font-size: 12px;
}
.route-view {
padding: 27px;
grid-template-columns: 1fr;
gap: 30px;
}
.route-view h2 {
font-size: 37px;
}
.route-view p:not(.eyebrow) {
font-size: 13px;
}
.notebook-art {
height: 250px;
}
.notebook-art i:nth-child(2) {
left: auto;
}
.workbench-note {
min-height: 220px;
}
.notes-art {
min-height: 220px;
}
.route-view ul {
font-size: 12px;
}
}
@media (max-width: 350px) {
.route-nav a {
font-size: 11px;
padding-inline: 10px;
}
.route-view {
padding: 23px;
}
.route-view h2 {
font-size: 33px;
}
}
</style>
</head>
<body>
<header class="header wrap">
<span class="brand"
><span class="mark" aria-hidden="true">m.</span>MRHTML / Workshop</span
>
<p>SMALL PATTERNS.<br />CONSIDERED DETAILS.</p>
</header>
<main class="wrap">
<div class="intro">
<p class="eyebrow">06 / ALLOWLISTED HASH ROUTER</p>
<h1>Three views. One document.</h1>
<p>
Explore a fictional workshop notebook. Routes select existing
sections, keep browser history, and leave unknown paths in a clear
not-found state.
</p>
</div>
<noscript
><p>
All three sections are shown and their links work as ordinary anchors.
JavaScript adds route selection and history-aware views.
</p></noscript
>
<div class="surface notebook">
<nav class="route-nav" aria-label="Notebook views">
<a href="#overview" data-route="overview">Overview</a
><a href="#workbench" data-route="workbench">Workbench</a
><a href="#notes" data-route="notes">Notes</a>
</nav>
<section
class="route-view"
id="overview"
aria-labelledby="heading-overview"
>
<div>
<p class="eyebrow">THE NOTEBOOK / 01</p>
<h2 id="heading-overview" tabindex="-1">
A place for<br />unfinished ideas.
</h2>
<p>
Keep the question, the experiment, and the next step together.
This notebook is sample content, ready to explore.
</p>
<a href="#workbench" data-route="workbench" class="route-link"
>Open the workbench →</a
>
</div>
<div class="notebook-art" aria-hidden="true">
<i></i><i></i><span>start<br />small.</span>
</div>
</section>
<section
class="route-view"
id="workbench"
aria-labelledby="heading-workbench"
>
<div>
<p class="eyebrow">THE NOTEBOOK / 02</p>
<h2 id="heading-workbench" tabindex="-1">
Make one thing<br />clearer.
</h2>
<p>
A compact workbench for the next version. These notes describe a
sample process; no tasks are submitted or synchronized.
</p>
<ul>
<li>Choose one question to answer.</li>
<li>Build a complete path through the idea.</li>
<li>Write down what surprised you.</li>
</ul>
</div>
<div class="workbench-note">
<span class="small">CURRENT QUESTION</span>
<p>
What would make<br />the next step<br /><strong>obvious?</strong>
</p>
</div>
</section>
<section class="route-view" id="notes" aria-labelledby="heading-notes">
<div>
<p class="eyebrow">THE NOTEBOOK / 03</p>
<h2 id="heading-notes" tabindex="-1">
Leave a note<br />for later.
</h2>
<p>
Good documentation keeps a choice understandable after the
conversation has ended.
</p>
<ul>
<li>What changed and why?</li>
<li>What remains outside the scope?</li>
<li>What evidence would change your mind?</li>
</ul>
</div>
<div class="notes-art" aria-hidden="true">
<span>01</span><i></i><i></i><i></i
><span>Keep the reason close.</span>
</div>
</section>
<section
class="route-view not-found"
id="not-found"
aria-labelledby="heading-not-found"
hidden
>
<div>
<p class="eyebrow">THIS VIEW IS NOT IN THE NOTEBOOK</p>
<h2 id="heading-not-found" tabindex="-1">Page not found.</h2>
<p id="unknown-route"></p>
<a class="route-link" href="#overview" data-route="overview"
>Return to overview →</a
>
</div>
</section>
</div>
<p
id="route-status"
class="status route-status"
role="status"
aria-live="polite"
aria-atomic="true"
>
All notebook sections are available below.
</p>
<p class="note">
The supported enhanced routes are <code>#/overview</code>,
<code>#/workbench</code>, and <code>#/notes</code>. This is a local view
switcher, not a server router.
</p>
</main>
<footer class="footer wrap">
<span>MRHTML / Original runnable example</span
><span>Local content. No network required.</span>
</footer>
<script>
(() => {
'use strict';
const keys = ['overview', 'workbench', 'notes'];
const views = keys.map((key) => document.getElementById(key));
const links = [...document.querySelectorAll('[data-route]')];
const titles = {
overview: 'Overview',
workbench: 'Workbench',
notes: 'Notes',
};
function render(focus) {
const raw = location.hash;
const key =
raw === ''
? 'overview'
: raw.startsWith('#/')
? raw.slice(2)
: raw.slice(1);
const known = keys.includes(key);
views.forEach((view) => {
view.hidden = !known || view.id !== key;
});
const missing = document.querySelector('#not-found');
missing.hidden = known;
document.querySelector('#unknown-route').textContent = known
? ''
: `Unknown route: ${raw.slice(0, 140)}${raw.length > 140 ? '…' : ''}`;
links.forEach((link) => {
if (known && link.dataset.route === key)
link.setAttribute('aria-current', 'page');
else link.removeAttribute('aria-current');
});
document.title = `MRHTML | ${known ? titles[key] : 'Page not found'}`;
document.querySelector('#route-status').textContent = known
? `${titles[key]} view. Only this document changed.`
: 'Route not found. Choose a notebook view.';
if (focus)
(known ? document.getElementById(key) : missing)
.querySelector('h2')
.focus({ preventScroll: false });
}
links.forEach((link) =>
link.addEventListener('click', (event) => {
event.preventDefault();
const key = link.dataset.route;
if (!keys.includes(key)) return;
const hash = '#/' + key;
if (location.hash === hash) render(true);
else location.hash = hash;
}),
);
window.addEventListener('hashchange', () => render(true));
render(Boolean(location.hash));
})();
</script>
</body>
</html>