├── src ├── state │ ├── index.js │ ├── Theme.js │ └── Menu.js ├── About.js ├── theme.js ├── hooks │ ├── index.js │ ├── useToggle.js │ ├── useLocalStorage.js │ ├── useScrollFreeze.js │ ├── useTheme.js │ ├── useMedia.js │ └── useScroll.js ├── index.js ├── index.css ├── components │ ├── nav │ │ ├── index.js │ │ ├── MobileNav.js │ │ ├── DesktopNav.js │ │ └── NavLinks.js │ └── Icon.js ├── App.js └── logo.svg ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── .gitignore ├── README.md └── package.json /src/state/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Menu"; 2 | export * from "./Theme"; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trujic1000/react-navbar/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trujic1000/react-navbar/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trujic1000/react-navbar/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/About.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const About = () => { 4 | return
About page
; 5 | }; 6 | 7 | export default About; 8 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | export const lightTheme = { 2 | bg: "#fff", 3 | text: "#000", 4 | }; 5 | 6 | export const darkTheme = { 7 | bg: "#232323", 8 | text: "#fff", 9 | }; 10 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | export * from "./useLocalStorage"; 2 | export * from "./useScroll"; 3 | export * from "./useScrollFreeze"; 4 | export * from "./useTheme"; 5 | export * from "./useToggle"; 6 | export * from "./useMedia"; 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text: #fff; 3 | --bg: #232323; 4 | --headerBg: #1c1c1c; 5 | --headerBoxShadow: rgba(0, 0, 0, 0.8) 0 1px 4px; 6 | } 7 | 8 | body.light { 9 | --text: #000; 10 | --bg: #f6f7f8; 11 | --headerBg: #fff; 12 | --headerBoxShadow: rgba(0, 0, 0, 0.15) 0 1px 4px; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useToggle.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const useToggle = (initialState) => { 4 | const [isToggled, setToggle] = useState(initialState); 5 | const toggle = () => setToggle((prevState) => !prevState); 6 | // return [isToggled, toggle]; 7 | return { isToggled, setToggle, toggle }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export const useLocalStorage = (key, initialValue) => { 4 | const item = window.localStorage.getItem(key); 5 | const [value, setValue] = useState(item || initialValue); 6 | 7 | useEffect(() => { 8 | window.localStorage.setItem(key, value); 9 | }, [value, key]); 10 | return [value, setValue]; 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Responsive Navbar 2 | 3 | This project uses the following technologies: 4 | 5 | - [React](https://reactjs.org) for frontend 6 | - [Styled Components](https://styled-components.com/) for styling the app 7 | 8 | ## Quick Start 9 | 10 | ```javascript 11 | // Install dependencies 12 | npm install or yarn 13 | 14 | // Run client 15 | npm run dev 16 | 17 | // Server runs on http://localhost:3000 18 | ``` 19 | -------------------------------------------------------------------------------- /src/hooks/useScrollFreeze.js: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from "react"; 2 | 3 | export const useScrollFreeze = (isMenuOpen) => { 4 | useLayoutEffect(() => { 5 | const original = window.getComputedStyle(document.body).overflow; 6 | 7 | if (isMenuOpen) { 8 | document.body.style.overflow = "hidden"; 9 | } 10 | 11 | return () => { 12 | document.body.style.overflow = original; 13 | }; 14 | }, [isMenuOpen]); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/nav/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import DesktopNav from "./DesktopNav"; 5 | import MobileNav from "./MobileNav"; 6 | 7 | const Navbar = () => { 8 | return ( 9 | 13 | ); 14 | }; 15 | 16 | export default Navbar; 17 | 18 | const Nav = styled.div` 19 | display: flex; 20 | flex-flow: column nowrap; 21 | `; 22 | -------------------------------------------------------------------------------- /src/hooks/useTheme.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useLocalStorage } from "./useLocalStorage"; 3 | 4 | export const useTheme = () => { 5 | const [theme, setTheme] = useLocalStorage("theme", "dark"); 6 | 7 | const toggleTheme = () => 8 | setTheme((prevTheme) => (prevTheme === "dark" ? "light" : "dark")); 9 | 10 | useEffect(() => { 11 | document.body.className = ""; 12 | document.body.classList.add(theme); 13 | }, [theme]); 14 | 15 | return [theme, toggleTheme]; 16 | }; 17 | -------------------------------------------------------------------------------- /src/hooks/useMedia.js: -------------------------------------------------------------------------------- 1 | import { useState, useLayoutEffect } from "react"; 2 | 3 | export const useMedia = () => { 4 | const [isMobile, setMobile] = useState(false); 5 | 6 | const onResize = () => { 7 | const isMobile = window.innerWidth < 768; 8 | setMobile(isMobile); 9 | }; 10 | 11 | useLayoutEffect(() => { 12 | window.addEventListener("resize", onResize); 13 | return () => { 14 | window.removeEventListener("resize", onResize); 15 | }; 16 | }, []); 17 | 18 | return { isMobile }; 19 | }; 20 | -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/state/Theme.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from "react"; 2 | import { useTheme } from "../hooks"; 3 | 4 | const initialState = { 5 | theme: "dark", 6 | toggleTheme: () => {}, 7 | }; 8 | 9 | export const ThemeContext = createContext(initialState); 10 | 11 | export const ThemeStateProvider = ({ children }) => { 12 | const [theme, toggleTheme] = useTheme(); 13 | return ( 14 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export const useThemeContext = () => { 26 | return useContext(ThemeContext); 27 | }; 28 | -------------------------------------------------------------------------------- /src/state/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from "react"; 2 | import { useToggle } from "../hooks"; 3 | 4 | const initialState = { 5 | isMenuOpen: false, 6 | toggle: () => {}, 7 | closeMenu: () => {}, 8 | }; 9 | 10 | export const MenuContext = createContext(initialState); 11 | 12 | export const MenuProvider = ({ children }) => { 13 | const { isToggled, setToggle, toggle } = useToggle(false); 14 | const closeMenu = () => setToggle(false); 15 | return ( 16 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export const useMenuContext = () => { 29 | return useContext(MenuContext); 30 | }; 31 | -------------------------------------------------------------------------------- /src/hooks/useScroll.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export const useScroll = () => { 4 | const [isScrolled, setIsScrolled] = useState(false); 5 | 6 | const onScroll = () => { 7 | const scrollTop = window !== undefined ? window.pageYOffset : 0; 8 | 9 | setIsScrolled(scrollTop > 0); 10 | }; 11 | 12 | useEffect(() => { 13 | // Learn more about how { passive: true } improves scrolling performance 14 | // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners 15 | window.addEventListener("scroll", onScroll, { passive: true }); 16 | return () => { 17 | window.removeEventListener("scroll", onScroll, { passive: true }); 18 | }; 19 | }, []); 20 | 21 | return { isScrolled }; 22 | }; 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 19 | React Responsive Navbar 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/nav/MobileNav.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { useMenuContext } from "../../state"; 4 | import { useScrollFreeze, useMedia } from "../../hooks"; 5 | import NavLinks from "./NavLinks"; 6 | 7 | const MobileNavbar = () => { 8 | const { isMenuOpen, closeMenu } = useMenuContext(); 9 | const { isMobile } = useMedia(); 10 | useScrollFreeze(isMenuOpen); 11 | 12 | useEffect(() => { 13 | if (!isMobile) { 14 | closeMenu(); 15 | } 16 | }, [isMobile]); 17 | 18 | return ( 19 | <> 20 | {isMenuOpen && ( 21 | 22 | 23 | 24 | )} 25 | 26 | ); 27 | }; 28 | 29 | export default MobileNavbar; 30 | 31 | const MobileNav = styled.nav` 32 | position: fixed; 33 | top: 0; 34 | left: 0; 35 | height: 100%; 36 | width: 100%; 37 | background: var(--bg); 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | `; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-navbar", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "hamburger-react": "^2.0.0", 10 | "prop-types": "^15.7.2", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-router-dom": "^5.1.2", 14 | "react-scripts": "3.4.1", 15 | "styled-components": "^5.1.0", 16 | "styled-reset": "^4.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 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 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Route } from "react-router-dom"; 3 | import { createGlobalStyle } from "styled-components"; 4 | import reset from "styled-reset"; 5 | import { MenuProvider } from "./state"; 6 | import Navbar from "./components/nav"; 7 | import About from "./About"; 8 | 9 | const GlobalStyle = createGlobalStyle` 10 | ${reset}; 11 | 12 | html { 13 | box-sizing: border-box; 14 | } 15 | 16 | body { 17 | font-family: "Montserrat", sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | background-color: var(--bg); 21 | color: var(--text); 22 | } 23 | 24 | * { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | *, 30 | *::before, 31 | *::after { 32 | box-sizing: inherit; 33 | } 34 | 35 | a { 36 | text-decoration: none; 37 | } 38 | `; 39 | 40 | const App = () => { 41 | return ( 42 | 43 |
44 | 45 | 46 | 47 | 48 |
Blabla
49 |
Blabla
50 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default App; 57 | -------------------------------------------------------------------------------- /src/components/nav/DesktopNav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import styled, { css } from "styled-components"; 4 | import { useMenuContext } from "../../state"; 5 | import { Squash as Hamburger } from "hamburger-react"; 6 | import NavLinks from "./NavLinks"; 7 | import { useScroll } from "../../hooks"; 8 | 9 | const DesktopNavbar = () => { 10 | const { isMenuOpen, toggleMenu } = useMenuContext(); 11 | const { isScrolled } = useScroll(); 12 | return ( 13 | 14 | 15 | Logo 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default DesktopNavbar; 24 | 25 | const DesktopNav = styled.nav` 26 | display: flex; 27 | flex-flow: row nowrap; 28 | justify-content: space-around; 29 | align-items: center; 30 | 31 | background: var(--bg); 32 | color: var(--text); 33 | transition: all 150ms linear; 34 | 35 | ${(props) => 36 | props.isScrolled && 37 | css` 38 | background: var(--headerBg); 39 | box-shadow: var(--headerBoxShadow); 40 | `} 41 | 42 | position: fixed; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | height: 64px; 47 | padding: 0 60px; 48 | z-index: 2; 49 | 50 | @media screen and (max-width: 768px) { 51 | justify-content: space-between; 52 | padding: 0 30px; 53 | } 54 | 55 | .logo { 56 | flex: 2; 57 | color: var(--text); 58 | font-size: 32px; 59 | } 60 | 61 | .nav-links { 62 | @media screen and (max-width: 768px) { 63 | display: none; 64 | } 65 | } 66 | 67 | .hamburger-react { 68 | display: none; 69 | z-index: 99; 70 | & > div > div { 71 | background: var(--text) !important; 72 | } 73 | @media screen and (max-width: 768px) { 74 | display: block; 75 | } 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /src/components/nav/NavLinks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Link } from "react-router-dom"; 4 | import { useMenuContext } from "../../state"; 5 | import { useTheme } from "../../hooks"; 6 | import Icon from "../Icon"; 7 | 8 | export const links = ["home", "about", "contact"]; 9 | 10 | const DesktopNavLinks = () => { 11 | const { closeMenu } = useMenuContext(); 12 | const [theme, toggleTheme] = useTheme(); 13 | 14 | return ( 15 | 16 | {links.map((link) => ( 17 |
  • 18 | 19 | {link} 20 | 21 |
  • 22 | ))} 23 |
  • 24 | 27 |
  • 28 |
    29 | ); 30 | }; 31 | 32 | export default DesktopNavLinks; 33 | 34 | const NavLinksWrapper = styled.ul` 35 | flex: 1; 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | list-style: none; 40 | 41 | li:not(:last-child) { 42 | margin-right: 26px; 43 | } 44 | 45 | li:last-child { 46 | margin-left: auto; 47 | } 48 | 49 | button { 50 | background: transparent; 51 | outline: none; 52 | border: none; 53 | cursor: pointer; 54 | } 55 | 56 | @media screen and (max-width: 768px) { 57 | flex-direction: column; 58 | li { 59 | padding: 12px; 60 | margin: 0 !important; 61 | } 62 | } 63 | `; 64 | 65 | export const NavLink = styled(Link)` 66 | position: relative; 67 | color: white; 68 | text-decoration: none; 69 | text-transform: capitalize; 70 | color: var(--text); 71 | &::before { 72 | content: ""; 73 | display: block; 74 | position: absolute; 75 | left: 0; 76 | bottom: -2px; 77 | height: 2px; 78 | width: 0; 79 | background: var(--text); 80 | transition: width 150ms linear; 81 | } 82 | &:hover::before { 83 | width: 100%; 84 | } 85 | `; 86 | -------------------------------------------------------------------------------- /src/components/Icon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Icon = ({ name, size = "24" }) => { 4 | switch (name) { 5 | case "day": 6 | return ( 7 | 13 | 17 | 18 | ); 19 | case "night": 20 | return ( 21 | 27 | 28 | 29 | ); 30 | default: 31 | return "Icon does not exist"; 32 | } 33 | }; 34 | 35 | export default Icon; 36 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------