Last active
September 8, 2019 20:05
-
-
Save xdmorgan/7b5e10493d87d8f4fe18d55d76d79ac6 to your computer and use it in GitHub Desktop.
React Animated Marquee (CSS Module, Custom Hook, Accessible)
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 React, { useRef, useState, useEffect } from 'react' | |
import cx from 'classnames' | |
import styles from './marquee-v2.module.scss' | |
export default function Marquee({ | |
children, | |
className = undefined, | |
reverse = false, | |
...props | |
}) { | |
const [count, ref, width] = useMarquee() | |
React.Children.only(children) | |
const [child] = React.Children.toArray(children) | |
return ( | |
<div | |
{...props} | |
className={cx( | |
styles.marquee, | |
{ | |
[styles.animated]: count !== null, | |
[styles.reversed]: !!reverse, | |
}, | |
className | |
)} | |
> | |
<div ref={ref} className={styles.marquee__measure} aria-hidden> | |
{children} | |
</div> | |
<div className={styles.marquee__spacer}>{children}</div> | |
<div className={styles.marquee__overflow}> | |
<div className={styles.marquee__elements} style={{ width }} aria-hidden> | |
{Array.from({ length: count }).map((_, idx) => | |
React.cloneElement(child, { | |
...child.props, | |
key: `marqueev2-${idx}`, | |
style: { ...child.props.style, flex: '0 0 auto' }, | |
}) | |
)} | |
</div> | |
</div> | |
</div> | |
) | |
} | |
const getWidth = el => el.clientWidth | |
function fillContainer(el) { | |
// get the individual element width and the container width as basis | |
// for inFullView calculation | |
const [single, total] = [getWidth(el), getWidth(el.parentNode)] | |
// the floored number of elements completely visible in the container | |
const inFullView = Math.floor(total / single) | |
// FillGaps: add one so there is never an empty space left out by the | |
// inFullView calculation e.g. 100px card in 150px contaienr. There | |
// would be 1 in full view but then a 50px gap | |
const fillGaps = 1 | |
// accountForAnimation: The animation pans the container of the repeated | |
// elements across the X access equal to the width of a single element | |
// in order to make sure there are no gaps whilst animating we'll need | |
// an additional 1 extra to make up for the one being animted offscreen. | |
const accountForAnimation = 1 | |
// combine & return | |
return inFullView + fillGaps + accountForAnimation | |
} | |
function useMarquee() { | |
const ref = useRef() | |
const [count, setCount] = useState(null) | |
useEffect(() => { | |
let throttle | |
function onUpdate() { | |
clearTimeout(throttle) | |
if (ref && ref.current) { | |
throttle = setTimeout(() => setCount(fillContainer(ref.current)), 500) | |
} | |
} | |
onUpdate() | |
window.addEventListener('resize', onUpdate) | |
return () => { | |
clearTimeout(throttle) | |
window.removeEventListener('resize', onUpdate) | |
} | |
}, [ref]) | |
return [count, ref, ref.current ? getWidth(ref.current) : null] | |
} |
Usage Example
import React from 'react'
import styles from './style.module.scss'
import { Marquee } from '../marquee'
export default function MarqueeExamples() {
return (
<>
<Marquee>
<div className={styles.cells}>
<div className={styles.cell}>Check out 👀</div>
<div className={styles.cell}>The demo 👀</div>
</div>
</Marquee>
<Marquee reverse>
<div className={styles.cells}>
<div className={styles.cell}>Reverse 👀</div>
<div className={styles.cell}>Direction 👀</div>
</div>
</Marquee>
</>
)
}
.cell {
&s {
display: flex;
}
font-family: 'Favorit Extended';
font-weight: 500;
font-size: 18px;
line-height: 84.42%;
display: flex;
padding: 0;
height: 40px;
align-items: center;
letter-spacing: -0.02em;
text-transform: uppercase;
color: var(--color-dark-navy);
&:nth-child(odd) {
color: var(--color-teal);
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Required Styles (Sass Module)