Last active
January 21, 2021 11:56
-
-
Save hungdoansy/a97cafaa70921747ef0682bd8bdc83a4 to your computer and use it in GitHub Desktop.
Simple Carousel with React and styled-component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Demo: https://codesandbox.io/s/elegant-frog-hbs3f?file=/src/Carousel.js:0-5654