web development

3 Dead-Simple Features to Add to Your Next.js Blog

7 min read
A stack of Rubik's cubes on a black table with a blue background

Introduction

Since I just built my blog last week, I thought I'd share some of the simple features I added that I think add some nice touches to the user experience. These features are very easy to implement and you can probably add all of them to your own blog in a couple of hours. Since my blog is built with styled-components, I'll be using it in the examples, but you can use any styling solution you prefer.

The features I'm going to talk about are:

  • Scroll progress bar
  • Scroll to top button
  • Reading time

You can see these features in action on this very page. If you scroll down, you'll see a progress bar at the top that shows you how much of the content you've read. You'll also see a button in the bottom right corner that allows you to quickly scroll back to the top of the page. And finally, at the top of the page, you'll see a small piece of text that tells you how long it will take to read the article.

1. Scroll progress bar

A scroll progres bar is a very simple feature that shows the user how much content they have read and how much is left. You might be wondering why this is useful since the browser already has a scroll bar, which is a good point to bring up. I personally like this approach because it's more visually appealing and is more intuitive to show a progress of something in a horizontal bar (which is how progress is visualized pretty much everywhere) rather than a vertical one.

To implement this feature, you might be tempted to use a library, but it's actually very easy to do with just a few lines of CSS and JavaScript. Here's how I did it in my blog:

ScrollProgress.tsx
import { useEffect, useState } from 'react';
 
import { ProgressBar } from './styles';
 
export default function ScrollProgress() {
  const [scrollProgress, setScrollProgress] = useState(0);
 
  const handleScroll = () => {
    const totalHeight =
      document.documentElement.scrollHeight -
      document.documentElement.clientHeight;
    const progress = window.scrollY / totalHeight;
    setScrollProgress(progress);
  };
 
  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
 
  return (
    <div>
      <ProgressBar $scrollProgress={scrollProgress} />
    </div>
  );
}

Here, I'm creating a component called ScrollProgress with a scrollProgress state variable that keeps track of the scroll progress in a numeric value between 0 and 1. In the useEffect hook, I'm adding an event listener to the scrollevent that calculates the scroll progress based on the current scroll position and the total height of the page. Note that I'm returning a cleanup function to remove the event listener when the component is unmounted. Then, I'm rendering a styled component called ProgressBar with the scroll progress as a prop.

styles.ts
import styled from 'styled-components';
 
export const ProgressBar = styled.div.attrs<{ $scrollProgress: number }>(
  (props) => ({
    style: {
      transform: `scaleX(${props.$scrollProgress})`,
    },
  }),
)`
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 0.25rem;
  background: var(--color-text-400);
  transform-origin: left;
`;

This styled component is what actually renders the progress bar. It sets the width of the bar to the scroll progress and scales it horizontally as the user scrolls.

A couple of things to note here:

  • I'm using transform to set the width of the progress bar instead of setting the width property directly because as it is a more performant way to update and animate the width of an element.
  • The transform property is defined inside the style attribute of the component using the attrs method from styled-components. This is the recommended way do it for frequently changing styles like this one (which changes on every scroll event)

Then all you need to do is import the ScrollProgress component wherever you intend to use it:

Post.tsx
import ScrollProgress from './ScrollProgress';
 
export default function Post({ mdxContent }) {
  const readingTime = calculateReadingTime(mdxContent);
 
  return (
    <div>
      ...
      <ScrollProgress />
      ...
    </div>
  );
}

That's it! You now have a scroll progress bar on your blog.

2. Scroll to top button

Continuing with the theme of scrolling, a scroll to top button is a nice feature to have not only for blogs but for any website with long content. It allows the user to quickly jump back to the top of the page without having to manually scroll. It's especially useful on mobile devices where scrolling can be a bit more cumbersome.

Here's how you can implement a scroll to top button in your blog:

ScrollToTop.tsx
import { useEffect, useState } from 'react';
import { FaAngleUp } from 'react-icons/fa6';
 
import { Button, Wrapper } from './styles';
 
export default function ScrollToTop() {
  const [isVisible, setIsVisible] = useState(false);
 
  const toggleVisibility = () => {
    setIsVisible(window.scrollY > 600);
  };
 
  useEffect(() => {
    window.addEventListener('scroll', toggleVisibility);
    return () => window.removeEventListener('scroll', toggleVisibility);
  }, []);
 
  const scrollToTop = () => {
    window.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  };
 
  if (!isVisible) return null;
 
  return (
    <Wrapper>
      <Button onClick={scrollToTop}>
        <FaAngleUp size={40} />
      </Button>
    </Wrapper>
  );
}

Here I'm an isVisible state variable to show or hide the button based on the scroll position. I have a toggleVisibility function that sets the isVisible state to true when the user has scrolled more than a certain amount of pixels (in this case, 600). I'm using the useEffect hook to add an event listener to the scroll event that calls the toggleVisibility function. And again, I'm returning a cleanup function to remove the event listener when the component is unmounted.

The scrollToTop function scrolls the window back to the top of the page when the button is clicked.

And, in case you're wondering, the FaAngleUp component is an icon from the react-icons library. You can use any icon you like here.

Then,my styled components look like this:

styles.ts
import styled from 'styled-components';
 
export const Wrapper = styled.div`
  position: fixed;
  bottom: 0;
  right: 0;
  left: 0;
  max-width: 50rem;
  margin: 0 auto;
`;
 
export const Button = styled.button`
  position: absolute;
  bottom: 1rem;
  right: 1rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
  text-transform: uppercase;
  border: none;
  border-radius: 100%;
  padding: 0.5rem;
  cursor: pointer;
  transition: background-color 200ms;
  color: var(--color-white);
  background-color: var(--color-primary-800);
 
  &:hover,
  &:focus {
    outline: none;
    background-color: var(--color-primary-700);
  }
 
  &:active {
    background-color: var(--color-primary-600);
  }
`;

The styles here are just an example, but the important part is that the Wrapper component is fixed to the bottom right corner of the page and the Button component is styled as a floating action button with a circular shape and a background color that matches the theme of the blog.

Then, you can import the ScrollToTop component and use it wherever you want to show the scroll to top button:

Post.tsx
import ScrollToTop from './ScrollToTop';
 
export default function Post(...) {
  return (
    <div>
      ...
      <ScrollToTop />
      ...
    </div>
  );
}

3. Reading time

The last simple feature I want to share with you today is a reading time indicator. This is a small piece of text that tells the user how long it will take to read the article. I like this feature because it's just as useful to the user - to know how much time they need to invest in reading the article - as it is to the author - to keep the content consise and to the point.

There are also libraries that can calculate the reading time for you, but it's very easy to do it yourself. I just added a couple of utility functions like this:

utils/reading-time.ts
function transformMDXToRawText(mdxContent: string): string {
  let rawText = mdxContent.replace(/<\/?[^>]+(>|$)/g, '');
 
  rawText = rawText
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'");
 
  rawText = rawText.trim().replace(/\s+/g, ' ');
 
  return rawText;
}
 
export default function calculateReadingTime(mdxContent: string): number {
  const averageReadingSpeed = 225;
 
  const rawText = transformMDXToRawText(mdxContent);
 
  const wordCount = rawText.split(/\s+/).length;
 
  const readingTimeMinutes = wordCount / averageReadingSpeed;
 
  const roundedReadingTime = Math.ceil(readingTimeMinutes);
 
  return roundedReadingTime;
}

I'm creating a utility function called calculateReadingTime that takes the MDX content as input, transforms it into raw text, calculates the word count, and then divides it by the average reading speed (it's between 200-250, so I used 225) to get the reading time in minutes. I'm also using another utility function called transformMDXToRawText to strip out the HTML tags and entities from the MDX content.

Then all you need to do is import the calculateReadingTime function and use it wherever you want to display the reading time:

Post.tsx
import calculateReadingTime from '../utils/reading-time';
 
export default function Post({ mdxContent }) {
  const readingTime = calculateReadingTime(mdxContent);
 
  return (
    <div>
      <p>Reading time: {readingTime} min</p>
      <MDXRenderer>{mdxContent}</MDXRenderer>
    </div>
  );
}

That's it! You now have a reading time indicator on your blog.

Putting it all together

If you implement all three of these features, your Post component might look something like this:

Post.tsx
import calculateReadingTime from '../utils/reading-time';
import ScrollProgress from './ScrollProgress';
import ScrollToTop from './ScrollToTop';
 
export default function Post({ mdxContent }) {
  const readingTime = calculateReadingTime(mdxContent);
 
  return (
    <div>
      <ScrollProgress />
      <ScrollToTop />
      <p>Reading time: {readingTime} min</p>
      <MDXRenderer>{mdxContent}</MDXRenderer>
    </div>
  );
}

Conclusion

And that's it! If you want to see how I built the rest of my blog, you can read my previous article on how I built my blog.

Thanks for reading! 🫐