├── .eslintrc ├── .gitignore ├── README.md ├── menu.gif ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── AnimatedNavbar.js ├── DemoControls.js ├── DropdownContainer │ ├── Components.js │ ├── FadeContents.js │ ├── index.js │ └── utils.js ├── DropdownContents │ ├── CompanyDropdown.js │ ├── Components.js │ ├── DevelopersDropdown.js │ └── ProductsDropdown.js ├── Navbar │ ├── NavbarItem.js │ └── index.js ├── index.css └── index.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "react/prop-types": 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A Stripe-style animated navbar menu 2 | Built with React, [Styled Components](https://www.styled-components.com/), and [React-Flip-Toolkit](https://github.com/aholachek/react-flip-toolkit). 3 | 4 | 5 | 6 | 7 | 8 | 1. [View the demo](https://aholachek.github.io/react-stripe-menu) 9 | 10 | 2. [Read the tutorial](https://css-tricks.com/building-a-complex-ui-animation-in-react-simply/) 11 | 12 | 13 | 3. [Check out the code for the main dropdown component](https://github.com/aholachek/react-stripe-menu/blob/master/src/DropdownContainer/index.js) 14 | 15 | ### Details 16 | 17 | This animation demo explores how one might recreate [Stripe's animated menu](https://stripe.com/) in React. 18 | 19 | In order to keep the example as simple as possible, it focuses mainly on the animation aspect and therefore is not WAI-ARIA compliant. (Take a look at Stripe's full implementation for what seems to be a fully accessible nav menu component.) 20 | 21 | There are multiple ways one could implement this animation, each with its own tradeoffs. This demo is particularly focused on developer ease of use. 22 | -------------------------------------------------------------------------------- /menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aholachek/react-stripe-menu/7e0ef85108642e75c6a8e13660e375e13dca431c/menu.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stripe-menu", 3 | "homepage": "http://aholachek.github.io/react-stripe-menu", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "normalize.css": "^8.0.1", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-flip-toolkit": "latest", 11 | "react-scripts": "3.0.1", 12 | "styled-components": "4.3.2" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject", 19 | "predeploy": "yarn run build", 20 | "deploy": "gh-pages -d build" 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^6.26.3", 24 | "babel-runtime": "^6.26.0", 25 | "gh-pages": "^1.1.0" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aholachek/react-stripe-menu/7e0ef85108642e75c6a8e13660e375e13dca431c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Stripe Menu 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/AnimatedNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import Navbar from "./Navbar" 3 | import NavbarItem from "./Navbar/NavbarItem" 4 | import { Flipper } from "react-flip-toolkit" 5 | import DropdownContainer from "./DropdownContainer" 6 | import CompanyDropdown from "./DropdownContents/CompanyDropdown" 7 | import DevelopersDropdown from "./DropdownContents/DevelopersDropdown" 8 | import ProductsDropdown from "./DropdownContents/ProductsDropdown" 9 | 10 | const navbarConfig = [ 11 | { title: "Products", dropdown: ProductsDropdown }, 12 | { title: "Developers", dropdown: DevelopersDropdown }, 13 | { title: "Company", dropdown: CompanyDropdown } 14 | ] 15 | 16 | export default class AnimatedNavbar extends Component { 17 | state = { 18 | activeIndices: [] 19 | } 20 | 21 | resetDropdownState = i => { 22 | this.setState({ 23 | activeIndices: typeof i === "number" ? [i] : [], 24 | animatingOut: false 25 | }) 26 | delete this.animatingOutTimeout 27 | } 28 | 29 | onMouseEnter = i => { 30 | if (this.animatingOutTimeout) { 31 | clearTimeout(this.animatingOutTimeout) 32 | this.resetDropdownState(i) 33 | return 34 | } 35 | if (this.state.activeIndices[this.state.activeIndices.length - 1] === i) 36 | return 37 | 38 | this.setState(prevState => ({ 39 | activeIndices: prevState.activeIndices.concat(i), 40 | animatingOut: false 41 | })) 42 | } 43 | 44 | onMouseLeave = () => { 45 | this.setState({ 46 | animatingOut: true 47 | }) 48 | this.animatingOutTimeout = setTimeout( 49 | this.resetDropdownState, 50 | this.props.duration 51 | ) 52 | } 53 | 54 | render() { 55 | const { duration } = this.props 56 | let CurrentDropdown 57 | let PrevDropdown 58 | let direction 59 | 60 | const currentIndex = this.state.activeIndices[ 61 | this.state.activeIndices.length - 1 62 | ] 63 | const prevIndex = 64 | this.state.activeIndices.length > 1 && 65 | this.state.activeIndices[this.state.activeIndices.length - 2] 66 | 67 | if (typeof currentIndex === "number") 68 | CurrentDropdown = navbarConfig[currentIndex].dropdown 69 | if (typeof prevIndex === "number") { 70 | PrevDropdown = navbarConfig[prevIndex].dropdown 71 | direction = currentIndex > prevIndex ? "right" : "left" 72 | } 73 | 74 | return ( 75 | 79 | 80 | {navbarConfig.map((n, index) => { 81 | return ( 82 | 88 | {currentIndex === index && ( 89 | 94 | 95 | {PrevDropdown && } 96 | 97 | )} 98 | 99 | ) 100 | })} 101 | 102 | 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/DemoControls.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import styled from "styled-components" 4 | 5 | const Form = styled.form` 6 | padding: 2rem 0 1rem 0; 7 | background-color: #fff; 8 | display: flex; 9 | justify-content: center; 10 | 11 | > div { 12 | border: 0; 13 | padding: 1rem 0 1rem 0; 14 | margin-right: 3rem; 15 | display: flex; 16 | } 17 | 18 | input { 19 | margin-right: 0.5rem; 20 | } 21 | label + label input { 22 | margin-left: 1.5rem; 23 | } 24 | b { 25 | margin-right: 1.5rem; 26 | } 27 | ` 28 | 29 | class DemoControls extends Component { 30 | static propTypes = { 31 | duration: PropTypes.number 32 | } 33 | 34 | render() { 35 | const { duration } = this.props 36 | return ( 37 |
(this.el = el)}> 38 |
39 | Speed 40 | {[["normal", 300], ["slow (for debugging)", 1000]].map( 41 | ([label, value]) => { 42 | return ( 43 | 57 | ) 58 | } 59 | )} 60 |
61 |
62 | ) 63 | } 64 | } 65 | 66 | export default DemoControls 67 | -------------------------------------------------------------------------------- /src/DropdownContainer/Components.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components" 2 | import { promoteLayer } from "./utils" 3 | 4 | const getDropdownRootKeyFrame = ({ animatingOut, direction }) => { 5 | if (!animatingOut && direction) return null 6 | return keyframes` 7 | from { 8 | transform: ${animatingOut ? "rotateX(0)" : "rotateX(-15deg)"}; 9 | opacity: ${animatingOut ? 1 : 0}; 10 | } 11 | to { 12 | transform: ${animatingOut ? "rotateX(-15deg)" : "rotateX(0)"}; 13 | opacity: ${animatingOut ? 0 : 1}; 14 | } 15 | ` 16 | } 17 | 18 | export const DropdownRoot = styled.div` 19 | transform-origin: 0 0; 20 | ${promoteLayer} 21 | animation-name: ${getDropdownRootKeyFrame}; 22 | animation-duration: ${props => props.duration}ms; 23 | /* use 'forwards' to prevent flicker on leave animation */ 24 | animation-fill-mode: forwards; 25 | /* flex styles will center the caret child component */ 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | position: relative; 30 | top: -20px; 31 | ` 32 | 33 | export const Caret = styled.div` 34 | width: 0; 35 | height: 0; 36 | border-width: 10px; 37 | border-style: solid; 38 | border-color: transparent transparent var(--white); 39 | /* make sure it's above the main dropdown container so now box-shadow bleeds over it */ 40 | z-index: 1; 41 | position: relative; 42 | /* prevent any gap in between caret and main dropdown */ 43 | top: 1px; 44 | ` 45 | 46 | export const DropdownBackground = styled.div` 47 | transform-origin: 0 0; 48 | background-color: var(--white); 49 | border-radius: 4px; 50 | overflow: hidden; 51 | position: relative; 52 | box-shadow: 0 50px 100px rgba(50, 50, 93, 0.1); 53 | ${promoteLayer} 54 | ` 55 | 56 | export const AltBackground = styled.div` 57 | background-color: var(--grey); 58 | width: 300%; 59 | height: 100%; 60 | position: absolute; 61 | top: 0; 62 | left: -100%; 63 | transform-origin: 0 0; 64 | z-index: 0; 65 | transition: transform ${props => props.duration}ms; 66 | ` 67 | 68 | export const InvertedDiv = styled.div` 69 | ${promoteLayer} 70 | position: ${props => (props.absolute ? "absolute" : "relative")}; 71 | top: 0; 72 | left: 0; 73 | &:first-of-type { 74 | z-index: 1; 75 | } 76 | &:not(:first-of-type) { 77 | z-index: -1; 78 | } 79 | ` 80 | -------------------------------------------------------------------------------- /src/DropdownContainer/FadeContents.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react" 2 | import PropTypes from "prop-types" 3 | import styled, { keyframes } from "styled-components" 4 | import { promoteLayer } from "./utils" 5 | 6 | const getFadeContainerKeyFrame = ({ animatingOut, direction }) => { 7 | if (!direction) return 8 | return keyframes` 9 | to { 10 | transform: translateX(0px); 11 | opacity: ${animatingOut ? 0 : 1}; 12 | } 13 | ` 14 | } 15 | const FadeContainer = styled.div` 16 | ${promoteLayer} 17 | animation-name: ${getFadeContainerKeyFrame}; 18 | animation-duration: ${props => props.duration}ms; 19 | animation-fill-mode: forwards; 20 | opacity: ${props => (props.direction && !props.animatingOut ? 0 : 1)}; 21 | top: 0; 22 | left: 0; 23 | ` 24 | 25 | const propTypes = { 26 | duration: PropTypes.number, 27 | direction: PropTypes.oneOf(["right", "left"]), 28 | animatingOut: PropTypes.bool, 29 | children: PropTypes.node 30 | } 31 | 32 | const FadeContents = forwardRef( 33 | ({ children, duration, animatingOut, direction }, ref) => ( 34 | 44 | ) 45 | ) 46 | 47 | FadeContents.propTypes = propTypes 48 | 49 | export default FadeContents 50 | -------------------------------------------------------------------------------- /src/DropdownContainer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Children, createRef } from "react" 2 | import PropTypes from "prop-types" 3 | import { Flipped } from "react-flip-toolkit" 4 | import { 5 | DropdownRoot, 6 | Caret, 7 | DropdownBackground, 8 | AltBackground, 9 | InvertedDiv 10 | } from "./Components" 11 | import FadeContents from "./FadeContents" 12 | 13 | const getFirstDropdownSectionHeight = el => { 14 | if ( 15 | !el || 16 | !el.querySelector || 17 | !el.querySelector("*[data-first-dropdown-section]") 18 | ) 19 | return 0 20 | return el.querySelector("*[data-first-dropdown-section]").offsetHeight 21 | } 22 | 23 | const updateAltBackground = ({ 24 | altBackground, 25 | prevDropdown, 26 | currentDropdown 27 | }) => { 28 | const prevHeight = getFirstDropdownSectionHeight(prevDropdown) 29 | const currentHeight = getFirstDropdownSectionHeight(currentDropdown) 30 | 31 | const immediateSetTranslateY = (el, translateY) => { 32 | el.style.transform = `translateY(${translateY}px)` 33 | el.style.transition = "transform 0s" 34 | requestAnimationFrame(() => (el.style.transitionDuration = "")) 35 | } 36 | 37 | if (prevHeight) { 38 | // transition the grey ("alt") background from its previous height to its current height 39 | immediateSetTranslateY(altBackground, prevHeight) 40 | requestAnimationFrame(() => { 41 | altBackground.style.transform = `translateY(${currentHeight}px)` 42 | }) 43 | } else { 44 | // just immediately set the background to the appropriate height 45 | // since we don't have a stored value 46 | immediateSetTranslateY(altBackground, currentHeight) 47 | } 48 | } 49 | 50 | class DropdownContainer extends Component { 51 | static propTypes = { 52 | children: PropTypes.node.isRequired, 53 | animatingOut: PropTypes.bool, 54 | direction: PropTypes.oneOf(["left", "right"]), 55 | duration: PropTypes.number 56 | } 57 | 58 | currentDropdownEl = createRef() 59 | prevDropdownEl = createRef() 60 | 61 | componentDidMount() { 62 | updateAltBackground({ 63 | altBackground: this.altBackgroundEl, 64 | prevDropdown: this.prevDropdownEl.current, 65 | currentDropdown: this.currentDropdownEl.current, 66 | duration: this.props.duration 67 | }) 68 | } 69 | 70 | render() { 71 | const { children, direction, animatingOut, duration } = this.props 72 | const [currentDropdown, prevDropdown] = Children.toArray(children) 73 | return ( 74 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | (this.altBackgroundEl = el)} 88 | duration={duration} 89 | /> 90 | 95 | {currentDropdown} 96 | 97 | 98 | 99 | 100 | 101 | 102 | {prevDropdown && ( 103 | 109 | {prevDropdown} 110 | 111 | )} 112 | 113 | 114 | 115 | 116 | 117 | ) 118 | } 119 | } 120 | 121 | export default DropdownContainer 122 | -------------------------------------------------------------------------------- /src/DropdownContainer/utils.js: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components" 2 | 3 | // if applied to a persistent component, make sure to remove when animation is not imminent 4 | // to prevent taking up too many browser resources with `will-change` 5 | export const promoteLayer = css` 6 | will-change: transform; 7 | ` 8 | -------------------------------------------------------------------------------- /src/DropdownContents/CompanyDropdown.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "styled-components" 3 | import { 4 | Heading, 5 | HeadingLink, 6 | LinkList, 7 | DropdownSection, 8 | Icon 9 | } from "./Components" 10 | 11 | const CompanyDropdownEl = styled.div` 12 | width: 18.5rem; 13 | ` 14 | 15 | const CompanyDropdown = () => { 16 | return ( 17 | 18 | 19 | 41 | 42 | 43 |
44 | 45 | From the Blog 46 | 47 | 48 |
  • 49 | Stripe Atlas › 50 |
  • 51 |
  • 52 | Stripe Home › 53 |
  • 54 |
  • 55 | Improved Fraud Detection › 56 |
  • 57 |
    58 |
    59 |
    60 |
    61 | ) 62 | } 63 | 64 | export default CompanyDropdown 65 | -------------------------------------------------------------------------------- /src/DropdownContents/Components.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | 3 | export const Heading = styled.h3` 4 | text-transform: uppercase; 5 | font-weight: bold; 6 | font-size: 1.1rem; 7 | margin-top: 0; 8 | margin-bottom: ${props => (props.noMarginBottom ? 0 : "1rem")}; 9 | color: ${({ color }) => (color ? `var(--${color})` : "var(--blue)")}; 10 | ` 11 | 12 | export const HeadingLink = Heading.withComponent("li") 13 | 14 | export const LinkList = styled.ul` 15 | li { 16 | margin-bottom: 1rem; 17 | } 18 | 19 | li:last-of-type { 20 | margin-bottom: 0; 21 | } 22 | 23 | margin-left: ${props => (props.marginLeft ? props.marginLeft : 0)}; 24 | ` 25 | 26 | export const Icon = styled.div` 27 | width: 13px; 28 | height: 13px; 29 | margin-right: 13px; 30 | background-color: var(--blue); 31 | opacity: 0.8; 32 | display: inline-block; 33 | ` 34 | 35 | export const DropdownSection = styled.div` 36 | padding: var(--spacer); 37 | position: relative; 38 | z-index: 1; 39 | ` 40 | -------------------------------------------------------------------------------- /src/DropdownContents/DevelopersDropdown.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "styled-components" 3 | import { 4 | Icon, 5 | DropdownSection, 6 | Heading, 7 | HeadingLink, 8 | LinkList 9 | } from "./Components" 10 | 11 | const DevelopersDropdownEl = styled.div` 12 | width: 25rem; 13 | ` 14 | 15 | const Flex = styled.div` 16 | display: flex; 17 | > div:first-of-type { 18 | margin-right: 48px; 19 | } 20 | ` 21 | 22 | const DevelopersDropdown = () => { 23 | return ( 24 | 25 | 26 |
    27 | Documentation 28 |

    Start integrating Stripe’s products and tools

    29 | 30 |
    31 |

    Get Started

    32 | 33 |
  • 34 | Elements 35 |
  • 36 |
  • 37 | Checkout 38 |
  • 39 |
  • 40 | Mobile apps 41 |
  • 42 |
    43 |
    44 |
    45 |

    Popular Topics

    46 | 47 |
  • 48 | Apple Pay 49 |
  • 50 |
  • 51 | Testing 52 |
  • 53 |
  • 54 | Launch Checklist 55 |
  • 56 |
    57 |
    58 |
    59 |
    60 |
    61 | 62 | 79 | 80 |
    81 | ) 82 | } 83 | 84 | export default DevelopersDropdown 85 | -------------------------------------------------------------------------------- /src/DropdownContents/ProductsDropdown.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "styled-components" 3 | import { Icon, DropdownSection, Heading } from "./Components" 4 | 5 | const ProductsDropdownEl = styled.div` 6 | width: 30rem; 7 | ` 8 | 9 | const Logo = styled.div` 10 | width: 38px; 11 | height: 38px; 12 | margin-right: 16px; 13 | border-radius: 100%; 14 | opacity: 0.6; 15 | background-color: ${({ color }) => `var(--${color})`}; 16 | ` 17 | 18 | const SubProductsList = styled.ul` 19 | li { 20 | display: flex; 21 | margin-bottom: 1rem; 22 | } 23 | h3 { 24 | margin-right: 1rem; 25 | width: 3.2rem; 26 | display: block; 27 | } 28 | a { 29 | color: var(--dark-grey); 30 | } 31 | ` 32 | 33 | const ProductsSection = styled.ul` 34 | li { 35 | display: flex; 36 | } 37 | ` 38 | 39 | const WorksWithStripe = styled.div` 40 | border-top: 2px solid #fff; 41 | display:flex; 42 | justify-content: center; 43 | align-items: center; 44 | margin-top: var(--spacer); 45 | padding-top: var(--spacer); 46 | } 47 | h3 { 48 | margin-bottom: 0; 49 | } 50 | ` 51 | 52 | const ProductsDropdown = () => { 53 | return ( 54 | 55 | 56 | 57 |
  • 58 |
    59 | 60 |
    61 |
    62 | Payments 63 |

    A complete payments platform

    64 |
    65 |
  • 66 |
  • 67 |
    68 | 69 |
    70 |
    71 | Billing 72 |

    Build and scale your recurring business model

    73 |
    74 |
  • 75 |
  • 76 |
    77 | 78 |
    79 |
    80 | Connect 81 |

    82 | Everything platforms need to get sellers paid 83 |

    84 |
    85 |
  • 86 |
    87 |
    88 | 89 | 90 |
  • 91 | Sigma 92 |
    Your business data at your fingertips.
    93 |
  • 94 |
  • 95 | Atlas 96 |
    The best way to start an internet business.
    97 |
  • 98 |
  • 99 | Radar 100 |
    Fight fraud with machine learning.
    101 |
  • 102 |
    103 | 104 | 105 | 106 | Works with Stripe 107 | 108 | 109 | 110 |
    111 |
    112 | ) 113 | } 114 | 115 | export default ProductsDropdown 116 | -------------------------------------------------------------------------------- /src/Navbar/NavbarItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import styled from "styled-components" 4 | 5 | const NavbarItemTitle = styled.button` 6 | background: transparent; 7 | border: 0; 8 | font-weight: bold; 9 | font-family: inherit; 10 | font-size: 18px; 11 | padding: 2rem 1.5rem 1.2rem 1.5rem; 12 | color: white; 13 | display: flex; 14 | justify-content: center; 15 | transition: opacity 250ms; 16 | cursor: pointer; 17 | /* position above the dropdown, otherwise the dropdown will cover up the bottom sliver of the buttons */ 18 | position: relative; 19 | z-index: 2; 20 | &:hover, &:focus { 21 | opacity: 0.7; 22 | outline:none; 23 | } 24 | ` 25 | 26 | const NavbarItemEl = styled.li` 27 | position: relative; 28 | ` 29 | 30 | const DropdownSlot = styled.div` 31 | position: absolute; 32 | left: 50%; 33 | transform: translateX(-50%); 34 | perspective: 1500px; 35 | ` 36 | 37 | export default class NavbarItem extends Component { 38 | static propTypes = { 39 | onMouseEnter: PropTypes.func.isRequired, 40 | title: PropTypes.string.isRequired, 41 | index: PropTypes.number.isRequired, 42 | children: PropTypes.node 43 | } 44 | onMouseEnter = () => { 45 | this.props.onMouseEnter(this.props.index) 46 | } 47 | 48 | render() { 49 | const { title, children } = this.props 50 | return ( 51 | 52 | {title} 53 | {children} 54 | 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Navbar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import styled from "styled-components" 3 | 4 | const NavbarEl = styled.nav` 5 | margin: auto; 6 | ` 7 | 8 | const NavbarList = styled.ul` 9 | display: flex; 10 | justify-content: center; 11 | list-style: none; 12 | margin: 0; 13 | ` 14 | 15 | class Navbar extends Component { 16 | render() { 17 | const { children, onMouseLeave } = this.props 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | } 25 | 26 | export default Navbar 27 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --white: #fff; 7 | --grey: #f1f4f8b0; 8 | --dark-grey: #6b7c93; 9 | --green: #24b47e; 10 | --teal: #4F96CE; 11 | --blue: #6772e5; 12 | --dark-blue: #4F3EF5; 13 | --spacer: 28px; 14 | } 15 | 16 | body { 17 | font-family: -apple-system, system-ui, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | color: var(--dark-grey); 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | color: var(--blue); 25 | } 26 | 27 | a:hover, 28 | a:focus { 29 | color: var(--dark-blue); 30 | } 31 | 32 | ul { 33 | list-style: none; 34 | padding-left: 0; 35 | } 36 | 37 | p { 38 | margin-top: 0; 39 | margin-bottom: 1rem; 40 | } 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import ReactDOM from "react-dom" 3 | import AnimatedNavbar from "./AnimatedNavbar" 4 | import DemoControls from "./DemoControls" 5 | import styled from "styled-components" 6 | import "./index.css" 7 | import "normalize.css"; 8 | 9 | const AppContainer = styled.div` 10 | background: linear-gradient(150deg, #53f 15%, #05d5ff); 11 | display: flex; 12 | flex-direction: column; 13 | min-height: 100vh; 14 | 15 | > div:first-of-type { 16 | flex: 1 0 70vh; 17 | } 18 | ` 19 | 20 | class App extends Component { 21 | state = { duration: 300 } 22 | 23 | onChange = data => { 24 | this.setState(data) 25 | } 26 | 27 | render() { 28 | return ( 29 | 30 | 31 | 35 | 36 | ) 37 | } 38 | } 39 | 40 | ReactDOM.render(, document.querySelector("#root")) 41 | --------------------------------------------------------------------------------