Building a pager component (Part 1)

I've recently become enamored with pager components. I think it's a really versatile way to navigate because it's primary concern is handling gestures, but it can be used in a number of different contexts. Like anything, after you try to build one once, you'll start seeing them everywhere.

One of the nicest pagers I've come across is in Apple's Podcast app:

Apple Podcasts Pager

This will serve as a reference for what we will be building in this series. In this first part, we'll define what we want out of the component, and produce a minimal working example.

In future posts, we will look into setting up nice spring transitions between the pages, handling gestures, as well as some other stuff we might want our component to do.

Behaviour & Props

The component should be unopinionated about what its children render, meaning we should be doing as little styling as possible to them. Our ideal API would look something like this:

  <Pager>
<Screen 1 />
<Screen 2 />
<Screen 3 />
</Pager>

We want to focus our attention, and our component, on managing gestures and transitioning between children, but we don't need to know anything about the children. By being flexible in this way, our component will be really nice to work with because it will be simple to use, the markup will be clean, and it will be reusable in a variety of different contexts.

With that being said, let's define an interface for our component that is as minimal as possible. As we extend the component we will add a few more props, but for now this will work:

interface Props {
activeIndex: number;
children: React.ReactNode;
}

Initial Code Setup

Given that we will be receiving an activeIndex prop, we can determine an offset that the pager should have at any given time. For example, if the activeIndex is 2, our offset should be 2 screens to the right.

function Pager({ children, activeIndex }) {
<!-- if activeIndex = 2, our offset will be -200 -->
const offset = activeIndex * 100 * -1

// ...
}

Now all we have to do is wrap our children in a container and translate it to our computed offset:

function Pager({ children, activeIndex }) {
const offset = activeIndex * 100 * -1;

return (
<div
style=
>
{children}
</div>
);
}

One more thing -- we want our children to be lined up horizontally, so we can position these children side by side based on their index:

function Pager({ children, activeIndex }) {
const offset = activeIndex * 100 * -1;

const inlineChildren = React.Children.map(children, (element, index) => {
return (
<div
style=
>
{element}
</div>
);
});

return (
<div
style=
>
{inlineChildren}
</div>
);
}

Now we will have something that looks like this:

Static translation for pager

You can see that the pager translates to the correct offset based on the activeIndex prop. The blue-ish outline represents the offset that we're translating.

Hopefully it's clear where I'm going with this is -- the foundation for the core behaviour is captured in this minimal example. What we will look into next is tackling the transitions between screens when activeIndex changes.

Here is an example of the fleshed out code we just wrote

CodeSandbox

This component was inspired by one that I built for React Native -- if you're curious you can check out the source code here.

Cheers!