I have recently been working on an app for musicians to connect and be matched up with based on similar preferences. I wanted the UX/UI to be something like a tinder swiper where you can scroll through different profile cards. In the past I have used libraries like Bootstrap to achieve the carousel-like presentation, however, I wanted to challenge myself to build that out myself with vanilla JS within my React app.
My first thoughts were to use CSS animation with toggled classes to move the cards in and out of the screen, however, I quickly found this method ineffective. I soon knew I would have to use JS to solve this problem. So allow me to walk you through my process.
To start, I needed to have an array of data - recommended musicians to swipe through.This was relatively easy given I had stored those recommendations in my Redux state. Note, you don’t need Redux for this, I am just using it because I have redux implemented for the larger context of my application. All you really need is an array to map over.
For Redux, all I had to do was map my state to props in the recommended users component like so:
const mapStateToProps = (state) => {
return {
currentUser: state.currentUser.currentUser,
recommendedUsers: state.currentUser.recommendedUsers,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchUserRecs: () => dispatch(fetchUserRecs()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(RecommendedUsers);
I mapped my the fetch recommended users dispatch function to props as well so when this component mounted in the application, it would fetch this data.
Now was the time where I had to figure out how to actually implement the carousel-like behavior. After some experimentation, I decided that I would make the container for all the recommended user profiles to be a div that had an overflow hidden property, with a nowrap white-space property. This meant that the div could not break its line and would continue horizontally. I could then manipulate the scrollLeft margin of the container with JS to shift what is in view based on what card is shown. This is what the CSS looked like for the cards-container div, as well as the card class itself:
.cards-container {
height: 100%;
overflow: hidden;
white-space: nowrap;
}
.card {
display: inline-block;
width: 100%;
height: 100%;
padding: 1rem;
}
Next I had to define some state variables locally in the component itself. I needed to figure out what the index in the array of recommended users of the active card was, so that would be a variable. And then I need a variable to store the current scroll margin to implement. So my component state looked like this:
state = {
activeIndex: 0,
margin: 0,
};
My render function looked something like this:
const shownUserId = this.props?.recommendedUsers[this.state.activeIndex]?.id || null;
return (
<div className="recommended-users">
<div className="cards-container">
{this.props?.recommendedUsers?.map((u, index) => (
<div>
<PreviewUserCard
user={u}
currentUser={this.props.currentUser}
key={u.id}
cardChange={this.cardChange}
shownUserId={shownUserId}
/>
</div>
))}
</div>
</div>
);
Basically I was mapping a component called PreviewUserCard that rendered all a user's information for each user in the recommended array. I passed in a callback function called cardChange that could be executed within the PreviewUserCard component. In the PreviewUserCard there is a button for the user to click that triggers this callback. This function is what would control the scrollLeft margin and change the active index.
cardChange = (e) => {
if (this.state.activeIndex === this.props.recommendedUsers.length - 1) {
this.setState({ activeIndex: 0 });
this.setState({ margin: 0 });
} else {
this.setState((state) => ({
activeIndex: state.activeIndex + 1,
margin: state.margin + window.innerWidth
}));
}
};
Basically, this function first checks if the current activeIndex is at the end of the recommended users array, and if it is, resets the active index to the first card - 0, as well as sets the margin to 0 as well. Otherwise, it will increment the activeIndex by 1 to the next user in the array and set the margin to the window width in addition to the previous margin. This is because a card is the width of the window and by increasing the scrollLeft margin by 100% we are essentially displaying the next card in the div.
The last part of this puzzle is the incrementally set the scrollLeft value. If we changed it all at once, there would be no carousel effect at all. So I decided to write a function that would be executed whenever the component updated (it will execute whenever the cardChange function is called). This important function is called setMargin, which essentially increments the current scrollLeft value in smaller chunks to give it a nice flow and feeling of swiping. It looks like this:
setMargin = () => {
const container = document.querySelector(".cards-container");
let interval = setInterval(() => {
let i = container.scrollLeft;
if (i < this.state.margin) {
container.scrollLeft = i + window.innerWidth / 100;
if (container.scrollLeft >= this.state.margin) {
clearInterval(interval);
}
} else {
container.scrollLeft = i - window.innerWidth / 50;
if (container.scrollLeft <= this.state.margin) {
clearInterval(interval);
}
}
}, 1);
};
First we get the cards container element and set that to a variable. Then, we set an interval which takes the current value of that container scrollLeft margin. It then says, while this current scroll value is less than the component state's margin value (our target value), increment in small amounts the current scrollLeft value until we hit out target scrollLeft value and then clear the interval. If the current scroll value of the container is MORE than our target value, then that means we have reached the end of our array and have reset to 0. We then do a similar thing of changing the current scroll value until we hit our target, however this time we are decrementing down (and doing it faster for a nice effect).
And that's it! If you've successfully followed along, you now know how to implement this yourself. There probably is a better way to do this, and I would love to implement touch events and smoothing incrementation (now it is all linear so it could be more natural), but for now I am proud to have came up with this method. It would probably be faster to just use a library like React-Carousel, or Bootstrap's Carousel, but this was a fun and enjoyable challenge. Feel free to comment any other solutions you may have to creating a carousel-like presentation. Thanks for reading!
Top comments (5)
Redux for this? 🙁😳
Redux is totally not required! You really just need an array of something to map divs in the parent container. I happened to be using redux in the larger context of my bigger application with this component.
Its a nice start on how to hand code a carousel. Obviously in the end product you will have considerations for accesibilty, responsiveness and all the other edge cases too.
How is this accessible? For example, to people who need longer to view a particular item. Or to screen readers?
That’s a great question. Well I didn’t mention this, but I am using a button element for people to hit ‘Next’ with so the carousel doesn’t auto play at all. It’s not perfect by any means, and I’m not done building the final product either - so I will definitely need to look into how to make it more accessible than it is currently - I think screen readers will be the biggest challenge in that light.