├── README.md ├── components └── Carousel │ ├── CarouselContainer.js │ ├── CarouselSlot.js │ ├── Indicator.js │ ├── Wrapper.js │ └── index.js └── containers └── CarouselPage ├── CarouselItem.js └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # react-css-carousel 2 | A mobile-only carousel component built in React and CSS. 3 | 4 | A tutorial of how this carousel was built can be found on the [@incubation.ff](https://medium.com/@incubation.ff) Medium blog. Read [Part One](https://medium.com/@incubation.ff/build-your-own-css-carousel-in-react-part-one-86f71f6670ca) and [Part Two](https://medium.com/@incubation.ff/build-your-own-css-carousel-in-react-part-two-89ec247251ae) for more information. 5 | 6 | ## Features 7 | 8 | - A mobile-only carousel wrapper which accepts any number of child items 9 | - The carousel is cntrolled via swipe functionality 10 | - It also includes an indicator to show how many items are in the carousel, and which item is currently being viewed. 11 | 12 | ## Built using: 13 | 14 | - React 15 | - [Styled Components](https://www.styled-components.com/) 16 | - [React-Swipeable](https://github.com/dogfessional/react-swipeable) 17 | - [Lodash](https://lodash.com/) 18 | - Flexbox 😉 -------------------------------------------------------------------------------- /components/Carousel/CarouselContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const CarouselContainer = styled.div` 4 | display: flex; 5 | margin: 0 0 20px 20px; 6 | transition: ${(props) => props.sliding ? 'none' : 'transform 1s ease'}; 7 | 8 | transform: ${(props) => { 9 | if (props.numSlides === 1) return 'translateX(0%)' 10 | 11 | if (props.numSlides === 2) { 12 | if (!props.sliding && props.direction === 'next') return 'translateX(calc(-80% + 30px))' 13 | if (!props.sliding && props.direction === 'prev') return 'translateX(0%)' 14 | if (props.direction === 'prev') return 'translateX(calc(-80% + 30px))' 15 | return 'translateX(0%)' 16 | } 17 | 18 | if (!props.sliding) return 'translateX(calc(-80% - 20px))' 19 | if (props.direction === 'prev') return 'translateX(calc(2 * (-80% - 20px)))' 20 | return 'translateX(0%)' 21 | }}; 22 | ` 23 | 24 | export default CarouselContainer 25 | -------------------------------------------------------------------------------- /components/Carousel/CarouselSlot.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const CarouselSlot = styled.div` 4 | flex: 1 0 100%; 5 | flex-basis: 80%; 6 | margin-right: 20px; 7 | order: ${(props) => props.order}; 8 | opacity: ${(props) => { 9 | if (props.numSlides === 1) return 1 10 | if (props.numSlides === 2) return props.order === props.position ? 1 : 0.5 11 | return props.order === 1 ? 1 : 0.5 12 | }}; 13 | transition: opacity 1s ease; 14 | ` 15 | 16 | export default CarouselSlot; 17 | -------------------------------------------------------------------------------- /components/Carousel/Indicator.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import styled from 'styled-components' 3 | 4 | const Container = styled.div` 5 | margin-bottom: 20px; 6 | margin-top: -20px; 7 | ` 8 | 9 | const Pip = styled.span` 10 | background: ${(props) => (props.isCurrent) ? 'darkorange' : 'gainsboro'}; 11 | width: 60px; 12 | height: 5px; 13 | margin-right: 5px; 14 | display: inline-block; 15 | transition: background 0.5s ease; 16 | cursor: pointer; 17 | ` 18 | 19 | class Indicator extends Component { 20 | render() { 21 | const { length, position } = this.props 22 | 23 | return ( 24 | 25 | { 26 | Array.from({ length }, (pip, i) => 27 | () 31 | ) 32 | } 33 | 34 | ) 35 | } 36 | } 37 | 38 | Indicator.propTypes = { 39 | length: PropTypes.number, 40 | position: PropTypes.number 41 | }; 42 | 43 | export default Indicator; 44 | -------------------------------------------------------------------------------- /components/Carousel/Wrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | width: 100%; 5 | overflow: hidden; 6 | ` 7 | 8 | export default Wrapper; 9 | -------------------------------------------------------------------------------- /components/Carousel/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component, Children, cloneElement } from 'react'; 2 | import styled from 'styled-components' 3 | import Swipeable from 'react-swipeable' 4 | import { throttle } from 'lodash' 5 | 6 | import CarouselContainer from './CarouselContainer' 7 | import Wrapper from './Wrapper' 8 | import CarouselSlot from './CarouselSlot' 9 | import Indicator from './Indicator' 10 | 11 | const TitleSection = styled.div` 12 | margin: 20px; 13 | ` 14 | 15 | class Carousel extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | position: 0, 20 | direction: props.children.length === 2 ? 'prev' : 'next', 21 | sliding: false 22 | } 23 | } 24 | 25 | getOrder(itemIndex) { 26 | const { position } = this.state 27 | const { children } = this.props 28 | const numItems = children.length 29 | 30 | if (numItems === 2) return itemIndex 31 | 32 | if (itemIndex - position < 0) return numItems - Math.abs(itemIndex - position) 33 | return itemIndex - position 34 | } 35 | 36 | doSliding = (direction, position) => { 37 | this.setState({ 38 | sliding: true, 39 | direction, 40 | position 41 | }) 42 | 43 | setTimeout(() => { 44 | this.setState({ 45 | sliding: false 46 | }) 47 | }, 50) 48 | } 49 | 50 | nextSlide = () => { 51 | const { position } = this.state 52 | const { children } = this.props 53 | const numItems = children.length 54 | 55 | if (numItems === 2 && position === 1) return 56 | 57 | this.doSliding('next', position === numItems - 1 ? 0 : position + 1) 58 | } 59 | 60 | prevSlide = () => { 61 | const { position } = this.state 62 | const { children } = this.props 63 | const numItems = children.length 64 | 65 | if (numItems === 2 && position === 0) return 66 | 67 | this.doSliding('prev', position === 0 ? numItems - 1 : position - 1) 68 | } 69 | 70 | handleSwipe = throttle((isNext) => { 71 | const { children } = this.props 72 | const numItems = children.length || 1 73 | 74 | if (isNext && numItems > 1) { 75 | this.nextSlide() 76 | } else if (numItems > 1) { 77 | this.prevSlide() 78 | } 79 | }, 500, { trailing: false }) 80 | 81 | render() { 82 | const { title, children } = this.props 83 | const { sliding, direction, position } = this.state 84 | 85 | const childrenWithProps = Children.map(children, 86 | (child) => cloneElement(child, { 87 | numSlides: children.length || 1 88 | }) 89 | ) 90 | 91 | return ( 92 |
93 | 94 |

{ title }

95 | { childrenWithProps.length > 1 && 96 | () 100 | } 101 |
102 | 103 | this.handleSwipe(true) } 105 | onSwipingRight={ () => this.handleSwipe() } 106 | > 107 | 108 | 113 | { childrenWithProps.map((child, index) => ( 114 | 120 | {child} 121 | 122 | )) } 123 | 124 | 125 | 126 |
127 | ) 128 | } 129 | } 130 | 131 | Carousel.propTypes = { 132 | title: PropTypes.string, 133 | children: PropTypes.node 134 | }; 135 | 136 | export default Carousel; 137 | -------------------------------------------------------------------------------- /containers/CarouselPage/CarouselItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styled from 'styled-components' 3 | 4 | const Item = styled.div` 5 | background: darkorange; 6 | text-align: center; 7 | padding: 50px; 8 | color: white; 9 | ` 10 | 11 | function CarouselItem(props) { 12 | return ( 13 | Item {props.index} of {props.numSlides} 14 | ) 15 | } 16 | 17 | CarouselItem.propTypes = { 18 | index: PropTypes.number, 19 | numSlides: PropTypes.number 20 | } 21 | 22 | export default CarouselItem -------------------------------------------------------------------------------- /containers/CarouselPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Carousel from 'components/Carousel' 4 | import CarouselItem from './CarouselItem' 5 | 6 | export default class CarouselPage extends Component { 7 | render() { 8 | return ( 9 |
10 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ); 20 | } 21 | } 22 | --------------------------------------------------------------------------------