Navigation · Complete runnable example

Reading progress and return link

Show a small progress bar without interrupting the article.

Live example

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.

When to use it

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.

Try the important details

  1. Scroll through the article and inspect the progress at the bottom.
  2. Resize the viewport or change the content height to see the denominator update.
  3. Use the return link with a keyboard. Focus moves to the article heading, and reduced motion avoids smooth scrolling.

How it works

  • Scroll and resize events schedule a single pending animation frame for the measurement and update.
  • A nonpositive scroll range produces 100 percent without dividing by zero. Other values are clamped to the 0 to 100 range.
  • ResizeObserver notices body size changes. The progress element is labeled without a continuously announcing live region. The return control prevents fragment navigation so an opaque srcdoc preview stays inside its own document.

The complete source

The preview, copied text, and downloaded file use this same document.

HTML

<!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>