├── .DS_Store ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rhr-logo.png ├── rollup.config.js └── src ├── components ├── Access │ └── Access.js ├── CustomLink │ └── CustomLink.js ├── Home │ └── Home.js ├── MainMenu │ ├── MainMenu.js │ └── MainMenu.scss ├── MobileMenuToggle │ ├── MobileMenuToggle.js │ └── MobileMenuToggle.scss └── SubMenu │ └── SubMenu.js ├── index.js ├── styles.scss └── variables.scss /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liubov-js/react-header-responsive/cdbc3761e6c332a3d18d8493b12561a9fc33f7e5/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea 4 | *.tgz -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Liubov Krivtsova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | license 11 | 12 |

13 | 14 | # react-header-responsive 15 | 16 | A responsive header menu for React applications with submenu support 17 | 18 | ### Features 19 | 20 | - [x] Menu & Submenu 21 | - [x] Desktop version 22 | - [x] Mobile version 23 | - [x] Tablet support 24 | - [x] Current link highlighting 25 | - [x] Custom anchor component support 26 | - [x] Server-side rendering (SSR) support 27 | - [x] Overlapping (display invisible header over content) 28 | 29 | ### Installation 30 | 31 | ``` 32 | npm install react-header-responsive 33 | ``` 34 | 35 | ### Usage 36 | 37 | | Property | Description | 38 | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | `home` | _Optional._ Logo component | 40 | | `pages` | _Optional._ An array of objects with `name` and `link`, or `name` and `children` properties. If children is specified it is an array of objects with `name` and `link` properties | 41 | | `access` | _Optional._ Access component for login, registration, etc. | 42 | | `anchor` | _Optional._ Function with an anchor component | 43 | | `overlap` | _Optional._ Whether the header should overlap the content and be invisible | 44 | | `currentPath` | _Required for SSR only._ Used **only for server-side rendering (SSR)** instead of _window.location.pathname_ | 45 | 46 | ### Example 47 | 48 | The use of react-header-responsive in a real project can be seen [here](https://cucomm.com/). 49 | 50 | ```js 51 | import Header from 'react-header-responsive'; 52 | import logo from './rhr-logo.png'; 53 | 54 | function App() { 55 | const pages = [ 56 | { 57 | name: 'About', 58 | link: '/about', 59 | }, 60 | { 61 | name: 'Products', 62 | children: [ 63 | { 64 | name: 'Product-1', 65 | link: '/product1', 66 | }, 67 | { 68 | name: 'Product-2', 69 | link: '/product2', 70 | }, 71 | { 72 | name: 'Product-3', 73 | link: '/product3', 74 | }, 75 | ], 76 | }, 77 | { 78 | name: 'Pricing', 79 | link: '/pricing', 80 | }, 81 | ]; 82 | 83 | const Access = () => { 84 | return ( 85 |
86 | 87 | 88 |
89 | ); 90 | }; 91 | 92 | return ( 93 | <> 94 |
} 96 | pages={pages} 97 | anchor={(link, name, className) => ( 98 | 99 | {name} 100 | 101 | )} 102 | access={} 103 | overlap 104 | /> 105 | 106 | ); 107 | } 108 | ``` 109 | 110 | ### Contributing 111 | 112 | 1. Clone repo. 113 | 2. Create / checkout feature/{name}, or fix/{name} branch. 114 | 3. Install deps: npm. 115 | 4. Make your changes. 116 | 5. Commit: git commit. 117 | 6. Push: git push. 118 | 7. Open PR targeting the main branch. 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-header-responsive", 3 | "version": "1.1.2", 4 | "description": "A responsive header for React apps", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "prepare": "husky install" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.12.10", 13 | "@babel/preset-env": "^7.20.2", 14 | "@babel/preset-react": "^7.12.10", 15 | "@rollup/plugin-node-resolve": "^11.1.1", 16 | "babel-loader": "^8.2.2", 17 | "husky": "^8.0.0", 18 | "prettier": "2.8.4", 19 | "pretty-quick": "^3.1.3", 20 | "react": "^18.2.0", 21 | "rollup": "^2.38.4", 22 | "rollup-plugin-babel": "^4.4.0", 23 | "rollup-plugin-peer-deps-external": "^2.2.4", 24 | "rollup-plugin-postcss": "^4.0.0", 25 | "rollup-plugin-terser": "^7.0.2" 26 | }, 27 | "peerDependencies": { 28 | "react": "^18.2.0" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/liubov-js/react-header-responsive.git" 33 | }, 34 | "author": "Liubov Krivtsova", 35 | "license": "MIT", 36 | "keywords": [ 37 | "react", 38 | "header" 39 | ], 40 | "bugs": { 41 | "url": "https://github.com/liubov-js/react-header-responsive/issues" 42 | }, 43 | "homepage": "https://github.com/liubov-js/react-header-responsive#readme" 44 | } 45 | -------------------------------------------------------------------------------- /rhr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liubov-js/react-header-responsive/cdbc3761e6c332a3d18d8493b12561a9fc33f7e5/rhr-logo.png -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import external from 'rollup-plugin-peer-deps-external'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import postcss from 'rollup-plugin-postcss'; 6 | 7 | export default [ 8 | { 9 | input: './src/index.js', 10 | output: [ 11 | { 12 | file: 'dist/index.js', 13 | format: 'cjs', 14 | }, 15 | { 16 | file: 'dist/index.es.js', 17 | format: 'es', 18 | exports: 'named', 19 | }, 20 | ], 21 | plugins: [ 22 | postcss({ 23 | plugins: [], 24 | minimize: true, 25 | }), 26 | babel({ 27 | exclude: 'node_modules/**', 28 | presets: ['@babel/preset-react'], 29 | }), 30 | external(), 31 | resolve(), 32 | terser(), 33 | ], 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /src/components/Access/Access.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Access = ({ access }) => { 4 | return
{access}
; 5 | }; 6 | 7 | export default Access; 8 | -------------------------------------------------------------------------------- /src/components/CustomLink/CustomLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CustomLink = ({ page, subClass, currentPath, anchor }) => { 4 | const path = currentPath || window.location.pathname; 5 | 6 | if (anchor) { 7 | return anchor( 8 | page.link, 9 | page.name, 10 | `${subClass} ${ 11 | page.link === path ? 'rhr-active-color' : 'rhr-common-color' 12 | }` 13 | ); 14 | } 15 | 16 | return ( 17 | 23 | {page.name} 24 | 25 | ); 26 | }; 27 | 28 | export default CustomLink; 29 | -------------------------------------------------------------------------------- /src/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Home = ({ home }) => { 4 | return
{home}
; 5 | }; 6 | 7 | export default Home; 8 | -------------------------------------------------------------------------------- /src/components/MainMenu/MainMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SubMenu from '../SubMenu/SubMenu'; 3 | import CustomLink from '../CustomLink/CustomLink'; 4 | import './MainMenu.scss'; 5 | 6 | const MainMenu = ({ pages, access, anchor, currentPath }) => { 7 | return ( 8 | 36 | ); 37 | }; 38 | 39 | export default MainMenu; 40 | -------------------------------------------------------------------------------- /src/components/MainMenu/MainMenu.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables'; 2 | 3 | nav { 4 | height: 100%; 5 | margin: 60px 40px 0; 6 | max-width: 100%; 7 | } 8 | 9 | nav ul { 10 | height: 20px; 11 | list-style: none; 12 | margin: 0; 13 | display: block; 14 | float: left; 15 | padding: 0; 16 | position: relative; 17 | width: 100%; 18 | } 19 | 20 | nav a:hover, 21 | nav span:hover { 22 | background-color: inherit; 23 | } 24 | 25 | nav a:link, 26 | nav a:visited, 27 | nav span { 28 | text-decoration: none; 29 | display: block; 30 | font-size: 20px; 31 | text-align: left; 32 | } 33 | 34 | nav ul ul { 35 | display: none; 36 | position: inherit; 37 | top: 100%; 38 | margin-left: 24px; 39 | } 40 | 41 | nav ul li:hover > ul { 42 | position: absolute; 43 | top: 60px; 44 | width: max-content; 45 | display: contents; 46 | } 47 | 48 | nav ul li ul li { 49 | min-width: 170px; 50 | float: none; 51 | display: list-item; 52 | position: relative; 53 | background-color: $background-color; 54 | padding-left: 24px; 55 | } 56 | 57 | .rhr-arrow { 58 | margin-left: 6px; 59 | margin-bottom: 3px; 60 | border: solid $common-link-color; 61 | border-width: 0 1px 1px 0; 62 | display: inline-block; 63 | padding: 3px; 64 | transform: rotate(45deg); 65 | } 66 | 67 | .rhr-caret { 68 | color: $common-link-color; 69 | cursor: pointer; 70 | user-select: none; 71 | line-height: $line-height-main-menu; 72 | } 73 | 74 | .rhr-active-color { 75 | color: $active-link-color; 76 | } 77 | 78 | .rhr-common-color { 79 | color: $common-link-color; 80 | } 81 | 82 | .rhr-sub-menu-link { 83 | padding: 0; 84 | line-height: $line-height-sub-menu; 85 | } 86 | 87 | .rhr-main-menu-link { 88 | line-height: $line-height-sub-menu; 89 | padding: 0; 90 | } 91 | 92 | .rhr-main-access { 93 | display: flex; 94 | justify-content: center; 95 | } 96 | 97 | /* DESKTOP */ 98 | @media only screen and (min-width: 768px) { 99 | nav { 100 | margin: 0 0 0 15px; 101 | } 102 | 103 | nav a:hover, 104 | nav span:hover { 105 | color: $active-link-color; 106 | } 107 | 108 | nav span:hover > i { 109 | border: solid $active-link-color; 110 | border-width: 0 1px 1px 0; 111 | } 112 | 113 | nav ul li { 114 | margin: auto; 115 | display: inline-block; 116 | } 117 | 118 | nav ul li ul li { 119 | padding-left: 0; 120 | } 121 | 122 | nav ul { 123 | height: 100%; 124 | display: -webkit-box; /*flex;*/ 125 | } 126 | 127 | nav ul ul { 128 | margin-left: 0; 129 | position: inherit; 130 | } 131 | 132 | .rhr-main-access { 133 | display: none; 134 | } 135 | 136 | nav ul li:hover > ul { 137 | display: block; 138 | } 139 | 140 | .rhr-sub-menu-link { 141 | padding: 0 15px; 142 | } 143 | 144 | .rhr-main-menu-link { 145 | line-height: $line-height-main-menu; 146 | padding: 0 15px; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/components/MobileMenuToggle/MobileMenuToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './MobileMenuToggle.scss'; 3 | 4 | const MobileMenuToggle = (props) => { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 | ); 12 | }; 13 | 14 | export default MobileMenuToggle; 15 | -------------------------------------------------------------------------------- /src/components/MobileMenuToggle/MobileMenuToggle.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables'; 2 | 3 | .rhr-menu-toggle { 4 | display: inline-block; 5 | cursor: pointer; 6 | } 7 | 8 | .rhr-bar-1, 9 | .rhr-bar-2, 10 | .rhr-bar-3 { 11 | width: 30px; 12 | height: 3px; 13 | background-color: $common-link-color; 14 | margin: 6px 0; 15 | transition: 0.4s; 16 | } 17 | 18 | .rhr-toggled { 19 | position: fixed; 20 | float: right; 21 | right: 13px; 22 | } 23 | 24 | .rhr-toggled .rhr-bar-1 { 25 | -webkit-transform: rotate(-45deg) translate(-6px, 4px); 26 | transform: rotate(-45deg) translate(-6px, 4px); 27 | } 28 | 29 | .rhr-toggled .rhr-bar-2 { 30 | opacity: 0; 31 | } 32 | 33 | .rhr-toggled .rhr-bar-3 { 34 | -webkit-transform: rotate(45deg) translate(-9px, -7px); 35 | transform: rotate(45deg) translate(-9px, -7px); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/SubMenu/SubMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CustomLink from '../CustomLink/CustomLink'; 3 | 4 | const SubMenu = ({ pages, anchor, currentPath }) => { 5 | return ( 6 |
    7 | {pages.map((ch, i) => { 8 | return ( 9 |
  • 10 | 15 |
  • 16 | ); 17 | })} 18 |
19 | ); 20 | }; 21 | 22 | export default SubMenu; 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import MainMenu from './components/MainMenu/MainMenu'; 3 | import Home from './components/Home/Home'; 4 | import Access from './components/Access/Access'; 5 | import MobileMenuToggle from './components/MobileMenuToggle/MobileMenuToggle'; 6 | import './styles.scss'; 7 | 8 | const Header = ({ pages, anchor, home, access, currentPath, overlap }) => { 9 | const [isToggled, setIsToggled] = useState(false); 10 | 11 | const toggleMenu = () => { 12 | setIsToggled((prevState) => !prevState); 13 | }; 14 | 15 | useEffect(() => { 16 | setIsToggled(false); 17 | }, [currentPath]); 18 | 19 | return ( 20 |
21 |
22 | 23 |
24 |
29 | {pages && } 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Header; 42 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .rhr-header { 4 | display: flex; 5 | justify-content: space-between; 6 | background-color: $background-color; 7 | height: 60px; 8 | width: 100%; 9 | font-family: 'Roboto Light', serif; 10 | z-index: 10000; 11 | } 12 | 13 | .rhr-overlap { 14 | position: absolute; 15 | background-color: rgb(0, 0, 0, 0); 16 | } 17 | 18 | .rhr-menu-container { 19 | z-index: 10000; 20 | position: fixed; 21 | max-width: 350px; 22 | width: 350px; 23 | height: 100%; 24 | background-color: $background-color; 25 | -webkit-font-smoothing: antialiased; 26 | transform-origin: 0 0; 27 | right: 0; 28 | /*transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0);*/ 29 | } 30 | 31 | .rhr-menu-container-mobile-transform { 32 | transform: translate(100%, 0); 33 | } 34 | 35 | .rhr-logo { 36 | max-height: 100%; 37 | } 38 | 39 | .rhr-access, 40 | .rhr-home { 41 | height: 100%; 42 | display: flex; 43 | align-items: center; 44 | } 45 | 46 | .rhr-mobile-menu { 47 | z-index: 10000; 48 | margin: 13px; 49 | } 50 | 51 | .rhr-access-container { 52 | display: none; 53 | } 54 | 55 | @media only screen and (min-width: 768px) { 56 | .rhr-access-container { 57 | display: block; 58 | } 59 | 60 | .rhr-menu-container { 61 | z-index: 10000; 62 | position: relative; 63 | max-width: 100%; 64 | display: block; 65 | width: 100%; 66 | height: 0; 67 | margin: 0; 68 | padding: 0; 69 | } 70 | 71 | .rhr-menu-container-mobile-transform { 72 | transform: none; 73 | } 74 | 75 | .rhr-mobile-menu { 76 | display: none; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/variables.scss: -------------------------------------------------------------------------------- 1 | $active-link-color: orange; 2 | $common-link-color: rgb(0 60 95); 3 | $background-color: rgb(217 217 217); 4 | $line-height-main-menu: 60px; 5 | $line-height-sub-menu: 40px; 6 | --------------------------------------------------------------------------------