A Pen by CSS-Tricks on CodePen.
Created
February 20, 2024 09:58
-
-
Save pbnkp/a7fe034e5f347d7079f24741bf6110dd to your computer and use it in GitHub Desktop.
React Easy Carousel
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
<div id='root'></div> |
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
import styled from "https://cdn.skypack.dev/[email protected]"; | |
import * as React from "https://cdn.skypack.dev/[email protected]"; | |
import * as ReactDOM from "https://cdn.skypack.dev/[email protected]"; | |
const H1 = styled.h1` | |
text-align: center; | |
margin: 0; | |
padding-bottom: 10rem; | |
` | |
const Relative = styled.div` | |
position: relative; | |
` | |
const Flex = styled.div` | |
display: flex; | |
` | |
const HorizontalCenter = styled(Flex)` | |
justify-content: center; | |
margin-left: auto; | |
margin-right: auto; | |
max-width: 25rem; | |
` | |
const Container = styled.div` | |
height: stretch; | |
width: 100%; | |
background: #ecf0f1; | |
` | |
const Item = styled.div` | |
color: white; | |
font-size: 2rem; | |
text-transform: capitalize; | |
width: ${({size}) => `${size}rem`}; | |
height: ${({size}) => `${size}rem`}; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
` | |
function getPrevElement(list) { | |
const sibling = list[0].previousElementSibling | |
if (sibling instanceof HTMLElement) { | |
return sibling | |
} | |
return sibling | |
} | |
function getNextElement(list) { | |
const sibling = list[list.length - 1].nextElementSibling | |
if (sibling instanceof HTMLElement) { | |
return sibling | |
} | |
return null | |
} | |
function usePosition(ref) { | |
const [prevElement, setPrevElement] = React.useState(null) | |
const [nextElement, setNextElement] = React.useState(null) | |
React.useEffect(() => { | |
const element = ref.current | |
const update = () => { | |
const rect = element.getBoundingClientRect() | |
const visibleElements = Array.from(element.children).filter((child) => { | |
const childRect = child.getBoundingClientRect() | |
return childRect.left >= rect.left && childRect.right <= rect.right | |
}) | |
if (visibleElements.length > 0) { | |
setPrevElement(getPrevElement(visibleElements)) | |
setNextElement(getNextElement(visibleElements)) | |
} | |
} | |
update() | |
element.addEventListener('scroll', update, {passive: true}) | |
return () => { | |
element.removeEventListener('scroll', update, {passive: true}) | |
} | |
}, [ref]) | |
const scrollToElement = React.useCallback( | |
(element) => { | |
const currentNode = ref.current | |
if (!currentNode || !element) return | |
let newScrollPosition | |
newScrollPosition = | |
element.offsetLeft + | |
element.getBoundingClientRect().width / 2 - | |
currentNode.getBoundingClientRect().width / 2 | |
currentNode.scroll({ | |
left: newScrollPosition, | |
behavior: 'smooth', | |
}) | |
}, | |
[ref], | |
) | |
const scrollRight = React.useCallback(() => scrollToElement(nextElement), [ | |
scrollToElement, | |
nextElement, | |
]) | |
const scrollLeft = React.useCallback(() => scrollToElement(prevElement), [ | |
scrollToElement, | |
prevElement, | |
]) | |
return { | |
hasItemsOnLeft: prevElement !== null, | |
hasItemsOnRight: nextElement !== null, | |
scrollRight, | |
scrollLeft, | |
} | |
} | |
const CarouserContainer = styled(Relative)` | |
overflow: hidden; | |
` | |
const CarouselItem = styled.div` | |
flex: 0 0 auto; | |
margin-left: 1rem; | |
` | |
const CarouselButton = styled.button` | |
position: absolute; | |
cursor: pointer; | |
top: 50%; | |
z-index: 1; | |
transition: transform 0.1s ease-in-out; | |
background: white; | |
border-radius: 15px; | |
border: none; | |
padding: 0.5rem; | |
` | |
const LeftCarouselButton = styled(CarouselButton)` | |
left: 0; | |
transform: translate(-100%, -50%); | |
${CarouserContainer}:hover & { | |
transform: translate(0%, -50%); | |
} | |
visibility: ${({hasItemsOnLeft}) => (hasItemsOnLeft ? `all` : `hidden`)}; | |
` | |
const RightCarouselButton = styled(CarouselButton)` | |
right: 0; | |
transform: translate(100%, -50%); | |
${CarouserContainer}:hover & { | |
transform: translate(0%, -50%); | |
} | |
visibility: ${({hasItemsOnRight}) => (hasItemsOnRight ? `all` : `hidden`)}; | |
` | |
const CarouserContainerInner = styled(Flex)` | |
overflow-x: scroll; | |
scroll-snap-type: x mandatory; | |
-ms-overflow-style: none; | |
scrollbar-width: none; | |
// offset for children spacing | |
margin-left: -1rem; | |
&::-webkit-scrollbar { | |
display: none; | |
} | |
${CarouselItem} & { | |
scroll-snap-align: center; | |
} | |
` | |
const ArrowLeft = ({size = 30, color = '#000000'}) => ( | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
width={size} | |
height={size} | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke={color} | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
> | |
<path d="M19 12H6M12 5l-7 7 7 7" /> | |
</svg> | |
) | |
const ArrowRight = ({size = 30, color = '#000000'}) => ( | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
width={size} | |
height={size} | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke={color} | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
> | |
<path d="M5 12h13M12 5l7 7-7 7" /> | |
</svg> | |
) | |
function Carousel({children}) { | |
const ref = React.useRef() | |
const { | |
hasItemsOnLeft, | |
hasItemsOnRight, | |
scrollRight, | |
scrollLeft, | |
} = usePosition(ref) | |
return ( | |
<CarouserContainer> | |
<CarouserContainerInner ref={ref}> | |
{React.Children.map(children, (child, index) => ( | |
<CarouselItem key={index}>{child}</CarouselItem> | |
))} | |
</CarouserContainerInner> | |
<LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft} onClick={scrollLeft}> | |
<ArrowLeft /> | |
</LeftCarouselButton> | |
<RightCarouselButton | |
hasItemsOnRight={hasItemsOnRight} | |
onClick={scrollRight} | |
> | |
<ArrowRight /> | |
</RightCarouselButton> | |
</CarouserContainer> | |
) | |
} | |
const colors = [ | |
'#f1c40f', | |
'#f39c12', | |
'#e74c3c', | |
'#16a085', | |
'#2980b9', | |
'#8e44ad', | |
'#2c3e50', | |
'#95a5a6', | |
] | |
const colorsArray = colors.map((color) => ( | |
<Item | |
size={20} | |
style={{background: color, borderRadius: '20px', opacity: 0.9}} | |
key={color} | |
> | |
{color} | |
</Item> | |
)) | |
function App() { | |
return ( | |
<Container> | |
<H1>Easy Carousel</H1> | |
<HorizontalCenter> | |
<Carousel>{colorsArray}</Carousel> | |
</HorizontalCenter> | |
</Container> | |
) | |
} | |
ReactDOM.render( | |
<App/>, | |
document.getElementById('root') | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment