Skip to content

Instantly share code, notes, and snippets.

@hungdoansy
Last active January 21, 2021 11:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hungdoansy/a97cafaa70921747ef0682bd8bdc83a4 to your computer and use it in GitHub Desktop.
Save hungdoansy/a97cafaa70921747ef0682bd8bdc83a4 to your computer and use it in GitHub Desktop.
Simple Carousel with React and styled-component
// Codesandbox: https://codesandbox.io/s/elegant-frog-hbs3f?file=/src/Carousel.js:0-5654
import React, { useRef, useState } from "react";
import classNames from "classnames";
import styled from "styled-components";
const ArrowDownContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 1.25rem;
height: 1.25rem;
> svg {
width: 100%;
height: auto;
fill: currentColor;
}
`;
const ArrowDown = () => {
return (
<ArrowDownContainer>
<svg
x="0px"
y="0px"
width="451.847px"
height="451.847px"
viewBox="0 0 451.847 451.847"
>
<g>
<path
d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751
c12.354-12.354,32.388-12.354,44.748,0l171.905,171.915l171.906-171.909c12.359-12.354,32.391-12.354,44.744,0
c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"
/>
</g>
</svg>
</ArrowDownContainer>
);
};
const CarouselContainer = styled.div`
position: relative;
width: 250px;
height: 250px;
background-color: #ecf0f1;
div.controls-container {
position: absolute;
top: 50%;
transform: translate3d(0, -50%);
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0;
z-index: 5;
div.control {
width: 1.5rem;
height: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&.prev-control {
transform: rotate(90deg);
}
&.next-control {
transform: rotate(-90deg);
}
}
}
div.indicators-container {
position: absolute;
bottom: 0.5rem;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
z-index: 5;
> span.indicator {
display: inline-block;
width: 0.75rem;
height: 0.75rem;
border-radius: 9999px;
cursor: pointer;
background-color: #95a5a6;
&.active {
background-color: #f1c40f;
}
}
}
div.carousel-viewport {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
display: flex;
overflow-x: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
}
.scrollbar-hidden {
scrollbar-color: transparent transparent; /* thumb and track color */
scrollbar-width: 0px;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
background: transparent;
width: 0;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: transparent;
border: none;
}
}
.carousel-slide {
position: relative;
width: 100%;
height: 100%;
flex: 0 0 100%;
background-color: #f99;
counter-increment: item;
&:nth-child(even) {
background-color: #99f;
}
&:before {
content: counter(item);
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -40%, 70px);
color: #fff;
font-size: 2em;
}
}
.carousel-snap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
scroll-snap-align: center;
}
`;
const useMultipleRefs = (n) => {
const refs = useRef(
Array(n)
.fill(1)
.reduce((acc, _, index) => {
acc[index] = React.createRef();
return acc;
}, [])
);
return refs.current;
};
const scrollIntoView = (ref) => {
ref.current.scrollIntoView({
behavior: "smooth",
block: "nearest"
});
};
export const SimpleCarousel = () => {
const numberOfPhotos = 4;
const [currentIndex, setCurrentIndex] = useState(0);
const refs = useMultipleRefs(numberOfPhotos);
const handleClickPrev = () => {
if (numberOfPhotos === 0 || numberOfPhotos === 1) {
return;
}
const prevItemIndex = (currentIndex + numberOfPhotos - 1) % numberOfPhotos;
scrollIntoView(refs[prevItemIndex]);
setCurrentIndex(prevItemIndex);
};
const handleClickNext = () => {
if (numberOfPhotos === 0 || numberOfPhotos === 1) {
return;
}
const nextItemIndex = (currentIndex + 1) % numberOfPhotos;
scrollIntoView(refs[nextItemIndex]);
setCurrentIndex(nextItemIndex);
};
const handleClickIndicator = (index) => {
if (numberOfPhotos === 0 || numberOfPhotos === 1) {
return;
}
scrollIntoView(refs[index]);
setCurrentIndex(index);
};
return (
<CarouselContainer>
<div className="controls-container">
<div onClick={handleClickPrev} className="control prev-control">
<ArrowDown />
</div>
<div onClick={handleClickNext} className="control next-control">
<ArrowDown />
</div>
</div>
<div className="indicators-container">
{Array(numberOfPhotos)
.fill(1)
.map((_, i) => (
<span
key={i}
className={classNames(
"indicator",
currentIndex === i && "active"
)}
onClick={() => handleClickIndicator(i)}
></span>
))}
</div>
<div className="carousel-viewport scrollbar-hidden">
{Array(numberOfPhotos)
.fill(1)
.map((_, i) => (
<div key={i} ref={refs[i]} className="carousel-slide">
<div className="carousel-snap" />
</div>
))}
</div>
</CarouselContainer>
);
};
@hungdoansy
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment