On a tech blog, we spend a lot of time polishing our content, our SEO, and our architecture. But what about our readers? Are they in the best conditions to consume our long tutorials?

User Experience (UX) isn’t just about grand animations. It often lies in micro-interactions. Today, I’m sharing 3 easy-to-implement features on your Astro site to turn reading into a real pleasure.

1. The Reading Progress Bar

When tackling a 2000-word article, it’s reassuring to know where you are. A discreet progress bar fixed at the top of the screen is an excellent visual indicator.

The Implementation: A simple empty <div> and a dash of JavaScript are all you need.

<div id="reading-progress" class="fixed top-0 left-0 h-1 bg-blue-500 z-50 transition-all duration-150" style="width: 0%;"></div>

<script>
  function initProgressBar() {
    const progressBar = document.getElementById('reading-progress');
    if (!progressBar) return;

    window.addEventListener('scroll', () => {
      // Total scrollable height = Document height - Window height
      const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
      const scrolled = window.scrollY;
      
      const progress = (scrolled / scrollableHeight) * 100;
      progressBar.style.width = `${progress}%`;
    });
  }

  // Compatible with View Transitions!
  document.addEventListener('astro:page-load', initProgressBar);
</script>

2. The Dynamic Table of Contents (TOC)

If your article is a guide, let your readers jump directly to the section that interests them. The advantage with Astro is that Markdown heading extraction is native.

The Implementation When rendering your content, Astro provides a headings property that you can use to build your menu.

---
// In your article component
const { post } = Astro.props;
const { Content, headings } = await post.render();

// We only keep H2 and H3 to avoid clutter
const toc = headings.filter((heading) => heading.depth === 2 || heading.depth === 3);
---

<aside class="toc-container">
  <h3>Table of Contents</h3>
  <ul>
    {toc.map((heading) => (
      <li class={`depth-${heading.depth}`}>
        <a href={`#${heading.slug}`}>{heading.text}</a>
      </li>
    ))}
  </ul>
</aside>

<Content />

Note: If you followed our previous i18n tutorial, remember that you can make the “Table of Contents” title dynamic based on the current URL!)

3. The “Copy Code” Button

This is THE must-have feature for any developer blog. Forcing your readers to manually select lines of code on mobile is torture. Give them a “Copy” button!

The Implementation Astro transforms your Markdown blocks into <pre><code> tags. We are going to dynamically inject a button into each <pre> tag using JavaScript.

<script>
  function initCopyButtons() {
    const blocks = document.querySelectorAll('pre');

    blocks.forEach((block) => {
      // We avoid duplicating if the button already exists
      if (block.querySelector('.copy-btn')) return;

      // Button creation
      const button = document.createElement('button');
      button.className = 'copy-btn absolute top-2 right-2 p-1 text-xs bg-gray-800 text-white rounded';
      button.innerText = 'Copy';

      // Relative positioning needed on the parent
      block.style.position = 'relative';
      block.appendChild(button);

      button.addEventListener('click', async () => {
        const code = block.querySelector('code')?.innerText;
        if (code) {
          await navigator.clipboard.writeText(code);
          button.innerText = 'Copied!';
          setTimeout(() => { button.innerText = 'Copy'; }, 2000);
        }
      });
    });
  }

  document.addEventListener('astro:page-load', initCopyButtons);
</script>

Conclusion

In less than 100 cumulative lines of code, we’ve just added three features that significantly improve reading comfort. Never forget: on the web, technology must always serve the user.

Do you have other micro-features you like to see on tech blogs?