Responsive disclosure navigation
NavigationA navigation bar that folds neatly into a keyboard-friendly disclosure on small screens.
HTML + CSS + JavaScript
aria-expanded="false" Navigation · Complete runnable example
Show a small progress bar without interrupting the article.
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 a progress indicator as optional orientation for a long article. The article remains readable without JavaScript. The return control is revealed only after its local focus-and-scroll handler is ready. Reaching 100 percent describes scroll position, not proof that the content was read.
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>A quieter kind of interface. | MRHTML example</title>
<style>
:root {
color-scheme: light;
--paper: #f5f3ee;
--surface: #fff;
--ink: #172a3b;
--muted: #536575;
--line: #d2dce1;
--blue: #234ed8;
--soft: #eef3ff;
--orange: #ad4b17;
--error: #a62440;
font:
16px/1.6 system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
color: var(--ink);
background: var(--paper);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: clamp(24px, 6vw, 64px) 22px;
min-height: 100svh;
}
main {
width: min(760px, 100%);
margin: auto;
min-width: 0;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: clamp(2rem, 6vw, 3rem);
line-height: 1.08;
letter-spacing: -0.055em;
font-weight: 680;
max-width: 680px;
}
h2 {
font-size: 1.2rem;
line-height: 1.3;
letter-spacing: -0.02em;
}
p {
color: var(--muted);
}
header {
margin-bottom: 28px;
}
.brand {
font:
700 0.73rem/1.5 ui-monospace,
monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--blue);
margin-bottom: 20px;
}
.intro {
max-width: 62ch;
margin-top: 16px;
}
.surface {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 18px;
padding: clamp(20px, 4vw, 32px);
}
.note {
font-size: 0.85rem;
margin-top: 20px;
}
.eyebrow {
font:
600 0.72rem/1.5 ui-monospace,
monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
}
button,
input,
textarea,
select {
font: inherit;
}
button {
border: 1px solid var(--ink);
background: var(--ink);
color: #fff;
border-radius: 8px;
padding: 10px 17px;
min-height: 44px;
font-weight: 650;
cursor: pointer;
}
button.secondary {
background: transparent;
color: var(--ink);
border-color: var(--line);
}
button:hover:not(:disabled) {
background: var(--blue);
border-color: var(--blue);
color: #fff;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
label {
font-weight: 600;
}
input:not([type='checkbox']):not([type='range']):not([type='file']),
textarea,
select {
width: 100%;
min-width: 0;
border: 1px solid #8e9faa;
border-radius: 8px;
padding: 10px 12px;
background: #fff;
color: var(--ink);
}
textarea {
resize: vertical;
min-height: 180px;
}
input[type='range'] {
width: 100%;
accent-color: var(--blue);
}
input[type='checkbox'] {
width: 20px;
height: 20px;
accent-color: var(--blue);
}
:focus-visible {
outline: 3px solid var(--orange);
outline-offset: 4px;
}
a {
color: var(--blue);
}
.status {
min-height: 1.6em;
font-size: 0.9rem;
margin-top: 18px;
overflow-wrap: anywhere;
}
.status[data-error='true'] {
color: var(--error);
}
[hidden] {
display: none !important;
}
.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;
}
.field {
display: grid;
gap: 7px;
margin-top: 16px;
}
footer {
font-size: 0.75rem;
color: var(--muted);
border-top: 1px solid var(--line);
margin-top: 30px;
padding-top: 16px;
display: flex;
justify-content: space-between;
gap: 12px;
}
noscript {
display: block;
padding: 14px;
background: #fff2d9;
border: 1px solid #ad4b17;
margin-top: 20px;
border-radius: 8px;
color: var(--ink);
}
@media (max-width: 400px) {
body {
padding: 25px 16px;
}
.surface {
padding: 20px;
}
button {
padding: 10px 14px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation: none !important;
transition: none !important;
}
}
main {
width: min(680px, 100%);
}
#reading-tools {
position: fixed;
z-index: 2;
top: 0;
left: 0;
right: 0;
height: 6px;
background: var(--line);
}
#reading-meter {
display: block;
width: 100%;
height: 6px;
appearance: none;
border: 0;
background: var(--line);
}
#reading-meter::-webkit-progress-bar {
background: var(--line);
}
#reading-meter::-webkit-progress-value {
background: var(--blue);
}
#reading-meter::-moz-progress-bar {
background: var(--blue);
}
#reading-value {
position: absolute;
right: 15px;
top: 16px;
background: var(--ink);
color: #fff;
padding: 4px 9px;
border-radius: 30px;
font:
600 0.7rem ui-monospace,
monospace;
}
.article-kicker {
font:
600 0.75rem ui-monospace,
monospace;
color: var(--orange);
padding-top: 10px;
scroll-margin-top: 30px;
}
.article-lead {
font-size: 1.25rem;
color: var(--ink);
line-height: 1.65;
margin: 22px 0 38px;
}
article section {
position: relative;
padding: 28px 0;
border-top: 1px solid var(--line);
}
article section h2 {
font-size: 1.7rem;
margin-bottom: 18px;
padding-right: 30px;
}
article section p:not(.section-number) {
margin: 18px 0;
line-height: 1.9;
}
.section-number {
font:
500 0.75rem ui-monospace,
monospace;
color: var(--blue);
margin-bottom: 14px;
}
#return-top {
display: inline-block;
margin-top: 18px;
font-weight: 650;
text-underline-offset: 5px;
}
</style>
</head>
<body>
<main>
<header>
<p class="brand">MRHTML / Runnable example</p>
<h1>A quieter kind of interface.</h1>
<p class="intro">
A short field note on making digital pages easier to read, navigate
and trust.
</p>
</header>
<div id="reading-tools" hidden>
<label class="sr-only" for="reading-meter">Page reading progress</label
><progress id="reading-meter" max="100" value="0"></progress
><span id="reading-value" aria-hidden="true">0%</span>
</div>
<article id="article" aria-labelledby="article-title">
<h2 id="article-title" tabindex="-1" class="article-kicker">
Field notes / Interface design
</h2>
<p class="article-lead">
The most useful details often make themselves felt before they are
noticed. A page can be deliberate without being loud.
</p>
<section>
<p class="section-number">01</p>
<h2>Start with the reader</h2>
<p>
A useful interface begins with a person trying to do something.
Before drawing a screen, write down the one thing that person needs
to leave with. A clear next step is often more valuable than another
feature. Keep the language close to the words people use when they
describe the task.
</p>
<p>
Look at the page as a sequence of decisions. Each heading should
explain what comes next, and each control should show the result it
can actually produce. Small, accurate labels reduce the effort of
learning an unfamiliar page.
</p>
</section>
<section>
<p class="section-number">02</p>
<h2>Give the page a rhythm</h2>
<p>
Space is part of the reading experience. A generous margin separates
one idea from another, while a tighter gap signals that pieces
belong together. Consistent spacing helps a page feel familiar
before every detail has been read.
</p>
<p>
Start with a short set of spacing values. Use them to connect
related labels, inputs and help text. Then step back and look at the
whole page at a narrow width. The rhythm should survive when a
paragraph becomes several lines longer.
</p>
</section>
<section>
<p class="section-number">03</p>
<h2>Make the next step clear</h2>
<p>
A button is a promise about the next moment. Its label should
describe the action, and the interface should acknowledge what
actually happened. If an operation fails, keep the work that the
reader has already done and explain a useful way forward.
</p>
<p>
Avoid using a success message for an action that only began. A file
offered for download is not proof that it reached a disk. A draft
saved in one browser is not a cloud backup. Honest language makes
the boundary of a small example easier to understand.
</p>
</section>
<section>
<p class="section-number">04</p>
<h2>Leave room for different ways of working</h2>
<p>
People navigate with keyboards, touch screens, pointing devices and
assistive technology. Native controls provide a strong starting
point because their behavior is familiar. Clear focus makes the
current location visible without changing the reading order.
</p>
<p>
Try a simple task without a mouse. Notice where focus moves after
something closes or disappears. If a control changes the visible
content, its label and state should still explain where the reader
is and what can happen next.
</p>
</section>
<section>
<p class="section-number">05</p>
<h2>Treat motion as an option</h2>
<p>
Motion can connect two states, but it can also compete with reading.
Keep essential information available when motion is reduced. For
decorative movement that continues alongside content, provide a
practical way to stop it.
</p>
<p>
A quiet transition should not become a requirement for understanding
the page. Compare the interface with transitions removed. If a
change is now confusing, improve its labels, position or state
before adding more animation.
</p>
</section>
<section>
<p class="section-number">06</p>
<h2>Finish with the small screens</h2>
<p>
A narrow viewport exposes assumptions about content. Long words,
numeric totals and groups of buttons should have somewhere to go.
Let text wrap where it remains meaningful, and use scrolling regions
only where a two-dimensional structure needs them.
</p>
<p>
The final check is a real reading pass. Move from the opening line
to the last action at a comfortable pace. Look for a heading that
arrives too late, a note that has lost its context or a control that
asks for more attention than its task deserves.
</p>
</section>
</article>
<a id="return-top" href="#article-title" hidden
>Return to the article start ↑</a
><noscript
>The article is readable without JavaScript. The reading progress
indicator and return control are unavailable.</noscript
>
<footer>
<span>MRHTML example</span><span>Original code · MIT</span>
</footer>
</main>
<script>
'use strict';
(() => {
const meter = document.querySelector('#reading-meter'),
value = document.querySelector('#reading-value');
let frame = 0;
function update() {
frame = 0;
const scroller =
document.scrollingElement || document.documentElement,
total = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
const progress =
total === 0
? 100
: Math.min(100, Math.max(0, (scroller.scrollTop / total) * 100));
meter.value = progress;
value.textContent = `${Math.round(progress)}%`;
}
function schedule() {
if (!frame) frame = requestAnimationFrame(update);
}
document.querySelector('#reading-tools').hidden = false;
document.addEventListener('scroll', schedule, { passive: true });
window.addEventListener('resize', schedule);
if ('ResizeObserver' in window)
new ResizeObserver(schedule).observe(document.body);
document
.querySelector('#return-top')
.addEventListener('click', (event) => {
event.preventDefault();
const title = document.querySelector('#article-title');
title.focus({ preventScroll: true });
title.scrollIntoView({
behavior: matchMedia('(prefers-reduced-motion: reduce)').matches
? 'instant'
: 'smooth',
block: 'start',
});
});
document.querySelector('#return-top').hidden = false;
update();
})();
</script>
</body>
</html>