├── .browserslistrc ├── .gitignore ├── .babelrc ├── postcss.config.js ├── public ├── assets │ ├── images │ │ ├── favicon.png │ │ └── sidebar-bg.jpg │ └── remixicon │ │ ├── remixicon.eot │ │ ├── remixicon.ttf │ │ ├── remixicon.woff │ │ ├── remixicon.woff2 │ │ ├── remixicon.css │ │ ├── remixicon.less │ │ ├── remixicon.symbol.svg │ │ └── remixicon.svg └── index.html ├── .prettierrc ├── src ├── styles │ ├── _variables.scss │ ├── _layout.scss │ ├── _sidebar.scss │ ├── styles.scss │ └── _menu.scss ├── libs │ ├── constants.js │ ├── poppers.js │ ├── popper.js │ └── slide.js └── index.js ├── .eslintrc ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── stale.yml ├── LICENSE ├── webpack.config.js ├── package.json └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['postcss-preset-env'], 3 | }; 4 | -------------------------------------------------------------------------------- /public/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azouaoui-med/pro-sidebar-template/HEAD/public/assets/images/favicon.png -------------------------------------------------------------------------------- /public/assets/images/sidebar-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azouaoui-med/pro-sidebar-template/HEAD/public/assets/images/sidebar-bg.jpg -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azouaoui-med/pro-sidebar-template/HEAD/public/assets/remixicon/remixicon.eot -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azouaoui-med/pro-sidebar-template/HEAD/public/assets/remixicon/remixicon.ttf -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azouaoui-med/pro-sidebar-template/HEAD/public/assets/remixicon/remixicon.woff -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azouaoui-med/pro-sidebar-template/HEAD/public/assets/remixicon/remixicon.woff2 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $text-color: #7d84ab; 2 | $secondary-text-color: #dee2ec; 3 | 4 | $bg-color: #0c1e35; 5 | $secondary-bg-color: #0b1a2c; 6 | 7 | $border-color: rgba(#535d7d, 0.3); 8 | 9 | $sidebar-header-height: 100px; 10 | $sidebar-footer-height: 230px; 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 12, 4 | "sourceType": "module" 5 | }, 6 | "env": { 7 | "browser": true, 8 | "es2021": true 9 | }, 10 | "settings": {}, 11 | "plugins": ["prettier"], 12 | "extends": ["airbnb-base", "prettier"], 13 | "rules": {} 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | .layout { 2 | z-index: 1; 3 | .header { 4 | display: flex; 5 | align-items: center; 6 | padding: 20px; 7 | } 8 | .content { 9 | padding: 12px 50px; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .footer { 15 | text-align: center; 16 | margin-top: auto; 17 | margin-bottom: 20px; 18 | padding: 20px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | - [ ] Bug fix 10 | - [ ] New feature 11 | - [ ] Documentation update 12 | - [ ] Refactoring / enhancement 13 | 14 | ## How Has This Been Tested? 15 | 16 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 17 | -------------------------------------------------------------------------------- /src/libs/constants.js: -------------------------------------------------------------------------------- 1 | export const ANIMATION_DURATION = 300; 2 | 3 | export const SIDEBAR_EL = document.getElementById('sidebar'); 4 | 5 | export const SUB_MENU_ELS = document.querySelectorAll( 6 | '.menu > ul > .menu-item.sub-menu' 7 | ); 8 | 9 | export const FIRST_SUB_MENUS_BTN = document.querySelectorAll( 10 | '.menu > ul > .menu-item.sub-menu > a' 11 | ); 12 | 13 | export const INNER_SUB_MENUS_BTN = document.querySelectorAll( 14 | '.menu > ul > .menu-item.sub-menu .menu-item.sub-menu > a' 15 | ); 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/libs/poppers.js: -------------------------------------------------------------------------------- 1 | import { SUB_MENU_ELS } from './constants'; 2 | import Popper from './popper'; 3 | 4 | class Poppers { 5 | subMenuPoppers = []; 6 | 7 | constructor() { 8 | this.init(); 9 | } 10 | 11 | init() { 12 | SUB_MENU_ELS.forEach((element) => { 13 | this.subMenuPoppers.push(new Popper(element, element.lastElementChild)); 14 | this.closePoppers(); 15 | }); 16 | } 17 | 18 | togglePopper(target) { 19 | if (window.getComputedStyle(target).visibility === 'hidden') 20 | target.style.visibility = 'visible'; 21 | else target.style.visibility = 'hidden'; 22 | } 23 | 24 | updatePoppers() { 25 | this.subMenuPoppers.forEach((element) => { 26 | element.instance.state.elements.popper.style.display = 'none'; 27 | element.instance.update(); 28 | }); 29 | } 30 | 31 | closePoppers() { 32 | this.subMenuPoppers.forEach((element) => { 33 | element.hide(); 34 | }); 35 | } 36 | } 37 | 38 | export default Poppers; 39 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | # Number of days of inactivity before a stale Issue or Pull Request is closed 6 | daysUntilClose: 7 7 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 8 | exemptLabels: 9 | - pinned 10 | - security 11 | # Label to use when marking as stale 12 | staleLabel: stale 13 | # Comment to post when marking as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when removing the stale label. Set to `false` to disable 19 | unmarkComment: false 20 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 21 | closeComment: false 22 | # Limit to only `issues` or `pulls` 23 | # only: issues 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mohamed AZOUAOUI 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 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | plugins: [ 7 | new MiniCssExtractPlugin(), 8 | new CopyWebpackPlugin({ 9 | patterns: [{ from: 'public' }], 10 | }), 11 | ], 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | use: { loader: 'babel-loader' }, 17 | exclude: /node_modules/, 18 | }, 19 | { 20 | test: /\.(css|scss|sass|less)$/i, 21 | use: [ 22 | MiniCssExtractPlugin.loader, 23 | 'css-loader', 24 | 'sass-loader', 25 | // 'less-loader', 26 | 'postcss-loader', 27 | ], 28 | }, 29 | { 30 | test: /\.(png|svg|jpg|gif)$/, 31 | use: ['file-loader'], 32 | }, 33 | { 34 | test: /\.(woff|woff2|eot|ttf|otf)$/, 35 | use: ['file-loader'], 36 | }, 37 | ], 38 | }, 39 | mode: 'development', 40 | target: 'web', 41 | devtool: 'source-map', 42 | devServer: { 43 | static: { 44 | directory: path.join(__dirname, 'public'), 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pro-sidebar-template", 3 | "version": "4.0.0", 4 | "main": "src/index.js", 5 | "repository": "https://github.com/azouaoui-med/pro-sidebar-template.git", 6 | "description": "Powerful and customizable side navigation", 7 | "author": "Mohamed Azouaoui ", 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "rimraf dist", 11 | "start": "webpack serve", 12 | "build": "rimraf dist && webpack", 13 | "gh-pages": "yarn build && gh-pages -d dist" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.16.0", 17 | "@babel/preset-env": "^7.16.4", 18 | "babel-loader": "^8.2.3", 19 | "copy-webpack-plugin": "^10.0.0", 20 | "copyfiles": "^2.4.1", 21 | "css-loader": "^5.2.7", 22 | "eslint": "^8.2.0", 23 | "eslint-config-airbnb-base": "15.0.0", 24 | "eslint-config-prettier": "^8.3.0", 25 | "eslint-plugin-import": "^2.25.2", 26 | "eslint-plugin-prettier": "^4.0.0", 27 | "file-loader": "^6.2.0", 28 | "gh-pages": "^3.2.3", 29 | "less": "^4.1.1", 30 | "less-loader": "^8.1.1", 31 | "mini-css-extract-plugin": "^1.6.0", 32 | "postcss": "^8.2.15", 33 | "postcss-loader": "^5.2.0", 34 | "postcss-preset-env": "^6.7.0", 35 | "prettier": "^2.5.0", 36 | "rimraf": "^3.0.2", 37 | "sass": "^1.43.4", 38 | "sass-loader": "^12.3.0", 39 | "style-loader": "^2.0.0", 40 | "webpack": "^5.64.2", 41 | "webpack-cli": "^4.9.1", 42 | "webpack-dev-server": "^4.5.0" 43 | }, 44 | "dependencies": { 45 | "@popperjs/core": "^2.9.2", 46 | "css-pro-layout": "^1.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/libs/popper.js: -------------------------------------------------------------------------------- 1 | import { createPopper } from '@popperjs/core'; 2 | import { SIDEBAR_EL } from './constants'; 3 | 4 | class Popper { 5 | instance = null; 6 | reference = null; 7 | popperTarget = null; 8 | 9 | constructor(reference, popperTarget) { 10 | this.init(reference, popperTarget); 11 | } 12 | 13 | init(reference, popperTarget) { 14 | this.reference = reference; 15 | this.popperTarget = popperTarget; 16 | this.instance = createPopper(this.reference, this.popperTarget, { 17 | placement: 'right', 18 | strategy: 'fixed', 19 | resize: true, 20 | modifiers: [ 21 | { 22 | name: 'computeStyles', 23 | options: { 24 | adaptive: false, 25 | }, 26 | }, 27 | { 28 | name: 'flip', 29 | options: { 30 | fallbackPlacements: ['left', 'right'], 31 | }, 32 | }, 33 | ], 34 | }); 35 | 36 | document.addEventListener( 37 | 'click', 38 | (e) => this.clicker(e, this.popperTarget, this.reference), 39 | false, 40 | ); 41 | 42 | const ro = new ResizeObserver(() => { 43 | this.instance.update(); 44 | }); 45 | 46 | ro.observe(this.popperTarget); 47 | ro.observe(this.reference); 48 | } 49 | 50 | clicker(event, popperTarget, reference) { 51 | if ( 52 | SIDEBAR_EL.classList.contains('collapsed') && 53 | !popperTarget.contains(event.target) && 54 | !reference.contains(event.target) 55 | ) { 56 | this.hide(); 57 | } 58 | } 59 | 60 | hide() { 61 | this.instance.state.elements.popper.style.visibility = 'hidden'; 62 | } 63 | } 64 | 65 | export default Popper; 66 | -------------------------------------------------------------------------------- /src/styles/_sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | color: $text-color; 3 | position: relative; 4 | height: 100%; 5 | max-height: 100%; 6 | overflow: hidden !important; 7 | 8 | .image-wrapper { 9 | overflow: hidden; 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | z-index: 1; 16 | display: none; 17 | > img { 18 | width: 100%; 19 | height: 100%; 20 | object-fit: cover; 21 | object-position: center; 22 | } 23 | } 24 | &.has-bg-image { 25 | .sidebar-layout { 26 | background-color: rgba($bg-color, 0.85); 27 | } 28 | .image-wrapper { 29 | display: block; 30 | // filter: blur(3px); 31 | } 32 | } 33 | 34 | .sidebar-layout { 35 | height: 100%; 36 | max-height: 100%; 37 | min-height: 100%; 38 | overflow-y: auto; 39 | display: flex; 40 | flex-direction: column; 41 | position: relative; 42 | background-color: $bg-color; 43 | z-index: 2; 44 | 45 | &::-webkit-scrollbar-thumb { 46 | border-radius: 4px; 47 | } 48 | 49 | &:hover { 50 | &::-webkit-scrollbar-thumb { 51 | background-color: lighten($bg-color, 15); 52 | } 53 | } 54 | 55 | &::-webkit-scrollbar { 56 | width: 6px; 57 | background-color: $bg-color; 58 | } 59 | 60 | .sidebar-header { 61 | height: $sidebar-header-height; 62 | min-height: $sidebar-header-height; 63 | display: flex; 64 | align-items: center; 65 | padding: 0 20px; 66 | > span { 67 | overflow: hidden; 68 | white-space: nowrap; 69 | text-overflow: ellipsis; 70 | } 71 | } 72 | .sidebar-content { 73 | flex-grow: 1; 74 | padding: 10px 0; 75 | } 76 | .sidebar-footer { 77 | height: $sidebar-footer-height; 78 | min-height: $sidebar-footer-height; 79 | display: flex; 80 | align-items: center; 81 | padding: 0 20px; 82 | > span { 83 | overflow: hidden; 84 | white-space: nowrap; 85 | text-overflow: ellipsis; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * https://remixicon.com 4 | * https://github.com/Remix-Design/RemixIcon 5 | * Copyright RemixIcon.com 6 | * Released under the Apache License Version 2.0 7 | */ 8 | 9 | @font-face { 10 | font-family: "remixicon"; 11 | src: url('remixicon.eot?t=1674579624758'); /* IE9*/ 12 | src: url('remixicon.eot?t=1674579624758#iefix') format('embedded-opentype'), /* IE6-IE8 */ 13 | url("remixicon.woff2?t=1674579624758") format("woff2"), 14 | url("remixicon.woff?t=1674579624758") format("woff"), 15 | url('remixicon.ttf?t=1674579624758') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 16 | url('remixicon.svg?t=1674579624758#remixicon') format('svg'); /* iOS 4.1- */ 17 | font-display: swap; 18 | } 19 | 20 | [class^="ri-"], [class*="ri-"] { 21 | font-family: 'remixicon' !important; 22 | font-style: normal; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; } 28 | .ri-xl { font-size: 1.5em; line-height: 0.6666em; vertical-align: -.075em; } 29 | .ri-xxs { font-size: .5em; } 30 | .ri-xs { font-size: .75em; } 31 | .ri-sm { font-size: .875em } 32 | .ri-1x { font-size: 1em; } 33 | .ri-2x { font-size: 2em; } 34 | .ri-3x { font-size: 3em; } 35 | .ri-4x { font-size: 4em; } 36 | .ri-5x { font-size: 5em; } 37 | .ri-6x { font-size: 6em; } 38 | .ri-7x { font-size: 7em; } 39 | .ri-8x { font-size: 8em; } 40 | .ri-9x { font-size: 9em; } 41 | .ri-10x { font-size: 10em; } 42 | .ri-fw { text-align: center; width: 1.25em; } 43 | 44 | .ri-bar-chart-2-fill:before { content: "\ea95"; } 45 | .ri-shopping-cart-fill:before { content: "\f11f"; } 46 | .ri-ink-bottle-fill:before { content: "\ee5c"; } 47 | .ri-book-2-fill:before { content: "\ead2"; } 48 | .ri-calendar-fill:before { content: "\eb26"; } 49 | .ri-global-fill:before { content: "\edce"; } 50 | .ri-service-fill:before { content: "\f0e1"; } 51 | .ri-menu-line:before { content: "\ef3e"; } 52 | .ri-vip-diamond-fill:before { content: "\f28f"; } 53 | .ri-arrow-left-s-line:before { content: "\ea64"; } 54 | .ri-github-fill:before { content: "\edca"; } 55 | .ri-twitter-fill:before { content: "\f23a"; } 56 | .ri-linkedin-box-fill:before { content: "\eeb3"; } 57 | .ri-codepen-fill:before { content: "\ebaf"; } 58 | -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.less: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * https://remixicon.com 4 | * https://github.com/Remix-Design/RemixIcon 5 | * Copyright RemixIcon.com 6 | * Released under the Apache License Version 2.0 7 | */ 8 | 9 | @font-face { 10 | font-family: "remixicon"; 11 | src: url('remixicon.eot?t=1674579624758'); /* IE9*/ 12 | src: url('remixicon.eot?t=1674579624758#iefix') format('embedded-opentype'), /* IE6-IE8 */ 13 | url("remixicon.woff2?t=1674579624758") format("woff2"), 14 | url("remixicon.woff?t=1674579624758") format("woff"), 15 | url('remixicon.ttf?t=1674579624758') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 16 | url('remixicon.svg?t=1674579624758#remixicon') format('svg'); /* iOS 4.1- */ 17 | font-display: swap; 18 | } 19 | 20 | [class^="ri-"], [class*="ri-"] { 21 | font-family: 'remixicon' !important; 22 | font-style: normal; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | :global { 28 | .ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; } 29 | .ri-xl { font-size: 1.5em; line-height: 0.6666em; vertical-align: -.075em; } 30 | .ri-xxs { font-size: .5em; } 31 | .ri-xs { font-size: .75em; } 32 | .ri-sm { font-size: .875em } 33 | .ri-1x { font-size: 1em; } 34 | .ri-2x { font-size: 2em; } 35 | .ri-3x { font-size: 3em; } 36 | .ri-4x { font-size: 4em; } 37 | .ri-5x { font-size: 5em; } 38 | .ri-6x { font-size: 6em; } 39 | .ri-7x { font-size: 7em; } 40 | .ri-8x { font-size: 8em; } 41 | .ri-9x { font-size: 9em; } 42 | .ri-10x { font-size: 10em; } 43 | .ri-fw { text-align: center; width: 1.25em; } 44 | 45 | .ri-bar-chart-2-fill:before { content: "\ea95"; } 46 | .ri-shopping-cart-fill:before { content: "\f11f"; } 47 | .ri-ink-bottle-fill:before { content: "\ee5c"; } 48 | .ri-book-2-fill:before { content: "\ead2"; } 49 | .ri-calendar-fill:before { content: "\eb26"; } 50 | .ri-global-fill:before { content: "\edce"; } 51 | .ri-service-fill:before { content: "\f0e1"; } 52 | .ri-menu-line:before { content: "\ef3e"; } 53 | .ri-vip-diamond-fill:before { content: "\f28f"; } 54 | .ri-arrow-left-s-line:before { content: "\ea64"; } 55 | .ri-github-fill:before { content: "\edca"; } 56 | .ri-twitter-fill:before { content: "\f23a"; } 57 | .ri-linkedin-box-fill:before { content: "\eeb3"; } 58 | .ri-codepen-fill:before { content: "\ebaf"; } 59 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './styles/styles.scss'; 2 | import { slideToggle, slideUp, slideDown } from './libs/slide'; 3 | import { 4 | ANIMATION_DURATION, 5 | FIRST_SUB_MENUS_BTN, 6 | INNER_SUB_MENUS_BTN, 7 | SIDEBAR_EL, 8 | } from './libs/constants'; 9 | import Poppers from './libs/poppers'; 10 | 11 | const PoppersInstance = new Poppers(); 12 | 13 | /** 14 | * wait for the current animation to finish and update poppers position 15 | */ 16 | const updatePoppersTimeout = () => { 17 | setTimeout(() => { 18 | PoppersInstance.updatePoppers(); 19 | }, ANIMATION_DURATION); 20 | }; 21 | 22 | /** 23 | * sidebar collapse handler 24 | */ 25 | document.getElementById('btn-collapse').addEventListener('click', () => { 26 | SIDEBAR_EL.classList.toggle('collapsed'); 27 | PoppersInstance.closePoppers(); 28 | if (SIDEBAR_EL.classList.contains('collapsed')) 29 | FIRST_SUB_MENUS_BTN.forEach((element) => { 30 | element.parentElement.classList.remove('open'); 31 | }); 32 | 33 | updatePoppersTimeout(); 34 | }); 35 | 36 | /** 37 | * sidebar toggle handler (on break point ) 38 | */ 39 | document.getElementById('btn-toggle').addEventListener('click', () => { 40 | SIDEBAR_EL.classList.toggle('toggled'); 41 | 42 | updatePoppersTimeout(); 43 | }); 44 | 45 | /** 46 | * toggle sidebar on overlay click 47 | */ 48 | document.getElementById('overlay').addEventListener('click', () => { 49 | SIDEBAR_EL.classList.toggle('toggled'); 50 | }); 51 | 52 | const defaultOpenMenus = document.querySelectorAll('.menu-item.sub-menu.open'); 53 | 54 | defaultOpenMenus.forEach((element) => { 55 | element.lastElementChild.style.display = 'block'; 56 | }); 57 | 58 | /** 59 | * handle top level submenu click 60 | */ 61 | FIRST_SUB_MENUS_BTN.forEach((element) => { 62 | element.addEventListener('click', () => { 63 | if (SIDEBAR_EL.classList.contains('collapsed')) 64 | PoppersInstance.togglePopper(element.nextElementSibling); 65 | else { 66 | /** 67 | * if menu has "open-current-only" class then only one submenu opens at a time 68 | */ 69 | const parentMenu = element.closest('.menu.open-current-submenu'); 70 | if (parentMenu) 71 | parentMenu 72 | .querySelectorAll(':scope > ul > .menu-item.sub-menu > a') 73 | .forEach( 74 | (el) => 75 | window.getComputedStyle(el.nextElementSibling).display !== 76 | 'none' && slideUp(el.nextElementSibling) 77 | ); 78 | slideToggle(element.nextElementSibling); 79 | } 80 | }); 81 | }); 82 | 83 | /** 84 | * handle inner submenu click 85 | */ 86 | INNER_SUB_MENUS_BTN.forEach((element) => { 87 | element.addEventListener('click', () => { 88 | slideToggle(element.nextElementSibling); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/libs/slide.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable no-unused-expressions */ 3 | import { ANIMATION_DURATION } from './constants'; 4 | 5 | export const slideUp = (target, duration = ANIMATION_DURATION) => { 6 | const { parentElement } = target; 7 | parentElement.classList.remove('open'); 8 | target.style.transitionProperty = 'height, margin, padding'; 9 | target.style.transitionDuration = `${duration}ms`; 10 | target.style.boxSizing = 'border-box'; 11 | target.style.height = `${target.offsetHeight}px`; 12 | target.offsetHeight; 13 | target.style.overflow = 'hidden'; 14 | target.style.height = 0; 15 | target.style.paddingTop = 0; 16 | target.style.paddingBottom = 0; 17 | target.style.marginTop = 0; 18 | target.style.marginBottom = 0; 19 | window.setTimeout(() => { 20 | target.style.display = 'none'; 21 | target.style.removeProperty('height'); 22 | target.style.removeProperty('padding-top'); 23 | target.style.removeProperty('padding-bottom'); 24 | target.style.removeProperty('margin-top'); 25 | target.style.removeProperty('margin-bottom'); 26 | target.style.removeProperty('overflow'); 27 | target.style.removeProperty('transition-duration'); 28 | target.style.removeProperty('transition-property'); 29 | }, duration); 30 | }; 31 | 32 | export const slideDown = (target, duration = ANIMATION_DURATION) => { 33 | const { parentElement } = target; 34 | parentElement.classList.add('open'); 35 | target.style.removeProperty('display'); 36 | let { display } = window.getComputedStyle(target); 37 | if (display === 'none') display = 'block'; 38 | target.style.display = display; 39 | const height = target.offsetHeight; 40 | target.style.overflow = 'hidden'; 41 | target.style.height = 0; 42 | target.style.paddingTop = 0; 43 | target.style.paddingBottom = 0; 44 | target.style.marginTop = 0; 45 | target.style.marginBottom = 0; 46 | target.offsetHeight; 47 | target.style.boxSizing = 'border-box'; 48 | target.style.transitionProperty = 'height, margin, padding'; 49 | target.style.transitionDuration = `${duration}ms`; 50 | target.style.height = `${height}px`; 51 | target.style.removeProperty('padding-top'); 52 | target.style.removeProperty('padding-bottom'); 53 | target.style.removeProperty('margin-top'); 54 | target.style.removeProperty('margin-bottom'); 55 | window.setTimeout(() => { 56 | target.style.removeProperty('height'); 57 | target.style.removeProperty('overflow'); 58 | target.style.removeProperty('transition-duration'); 59 | target.style.removeProperty('transition-property'); 60 | }, duration); 61 | }; 62 | 63 | export const slideToggle = (target, duration = ANIMATION_DURATION) => { 64 | if (window.getComputedStyle(target).display === 'none') return slideDown(target, duration); 65 | return slideUp(target, duration); 66 | }; 67 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | $sidebar-width: 250px; 2 | $sidebar-collapsed-width: 80px; 3 | 4 | @import 'css-pro-layout/dist/scss/css-pro-layout.scss'; 5 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap'); 6 | @import '../../public//assets//remixicon/remixicon.css'; 7 | @import './variables'; 8 | @import './layout'; 9 | @import './sidebar'; 10 | @import './menu'; 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | height: 100vh; 19 | font-family: 'Poppins', sans-serif; 20 | color: #3f4750; 21 | font-size: 0.9rem; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | } 27 | 28 | @media (max-width: $breakpoint-lg) { 29 | #btn-collapse { 30 | display: none; 31 | } 32 | } 33 | 34 | .layout { 35 | .sidebar { 36 | .pro-sidebar-logo { 37 | display: flex; 38 | align-items: center; 39 | 40 | > div { 41 | width: 35px; 42 | min-width: 35px; 43 | height: 35px; 44 | min-height: 35px; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | border-radius: 8px; 49 | color: white; 50 | font-size: 24px; 51 | font-weight: 700; 52 | background-color: #ff8100; 53 | margin-right: 10px; 54 | } 55 | 56 | > h5 { 57 | overflow: hidden; 58 | white-space: nowrap; 59 | text-overflow: ellipsis; 60 | font-size: 20px; 61 | line-height: 30px; 62 | transition: opacity 0.3s; 63 | opacity: 1; 64 | } 65 | } 66 | 67 | .footer-box { 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | justify-content: center; 72 | text-align: center; 73 | font-size: 0.8em; 74 | padding: 20px 0; 75 | border-radius: 8px; 76 | width: 180px; 77 | min-width: 190px; 78 | margin: 0 auto; 79 | background-color: #162d4a; 80 | img.react-logo { 81 | width: 40px; 82 | height: 40px; 83 | margin-bottom: 10px; 84 | } 85 | a { 86 | color: #fff; 87 | font-weight: 600; 88 | margin-bottom: 10px; 89 | } 90 | } 91 | 92 | .sidebar-collapser { 93 | transition: left, right, 0.3s; 94 | position: fixed; 95 | left: calc(#{$sidebar-width} - 20px); 96 | top: 40px; 97 | width: 20px; 98 | height: 20px; 99 | border-radius: 50%; 100 | background-color: #00829f; 101 | display: -webkit-box; 102 | display: -ms-flexbox; 103 | display: flex; 104 | -ms-flex-align: center; 105 | align-items: center; 106 | justify-content: center; 107 | font-size: 1.2em; 108 | transform: translateX(50%); 109 | z-index: 111; 110 | cursor: pointer; 111 | color: white; 112 | box-shadow: 1px 1px 4px $bg-color; 113 | } 114 | 115 | &.has-bg-image { 116 | .footer-box { 117 | background-color: rgba(#162d4a, 0.7); 118 | } 119 | } 120 | 121 | &.collapsed { 122 | .pro-sidebar-logo { 123 | > h5 { 124 | opacity: 0; 125 | } 126 | } 127 | .footer-box { 128 | display: none; 129 | } 130 | .sidebar-collapser { 131 | left: calc(#{$sidebar-collapsed-width} - 20px); 132 | i { 133 | transform: rotate(180deg); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | .badge { 141 | display: inline-block; 142 | padding: 0.25em 0.4em; 143 | font-size: 75%; 144 | font-weight: 700; 145 | line-height: 1; 146 | text-align: center; 147 | white-space: nowrap; 148 | vertical-align: baseline; 149 | border-radius: 0.25rem; 150 | color: #fff; 151 | background-color: #6c757d; 152 | 153 | &.primary { 154 | background-color: #ab2dff; 155 | } 156 | 157 | &.secondary { 158 | background-color: #079b0b; 159 | } 160 | } 161 | 162 | .sidebar-toggler { 163 | position: fixed; 164 | right: 20px; 165 | top: 20px; 166 | } 167 | 168 | .social-links { 169 | a { 170 | margin: 0 10px; 171 | color: #3f4750; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.symbol.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pro sidebar template 2 | 3 | Responsive layout with advanced sidebar menu built with SCSS and vanilla Javascript 4 | 5 | ## Demo 6 | 7 | [See it live](https://azouaoui-med.github.io/pro-sidebar-template) 8 | 9 | ## Screenshot 10 | 11 | ![Pro Sidebar](https://user-images.githubusercontent.com/25878302/215290325-e5c6043b-4411-404c-83b8-dcc227df70ad.jpg) 12 | 13 | ## Installation 14 | 15 | ``` 16 | # clone the repo 17 | $ git clone https://github.com/azouaoui-med/pro-sidebar-template.git my-project 18 | 19 | # go into app's directory 20 | $ cd my-project 21 | 22 | # install app's dependencies 23 | $ yarn install 24 | 25 | ``` 26 | 27 | ## Usage 28 | 29 | ``` 30 | # serve with hot reload at localhost:8080 31 | $ yarn start 32 | 33 | # build app for production 34 | $ yarn build 35 | 36 | ``` 37 | 38 | ## Documentation 39 | 40 | ### Layout 41 | 42 | The layout for this template is based on [css pro layout](https://github.com/azouaoui-med/css-pro-layout) package, please refer to the [docs](https://azouaoui-med.github.io/css-pro-layout/) for more information 43 | 44 | ### Sidebar 45 | 46 | Responsive navigation element for building vertical menu items 47 | 48 | **Sidebar Image** 49 | 50 | Adding background image requires adding `.has-bg-image` class to sidebar component, and the image needs to be inside `.image-wrapper` component 51 | 52 | ```html 53 | 61 | ``` 62 | 63 | ### Sidebar Layout 64 | 65 | Sidebar comes with layout support for better organization of the inner structure 66 | 67 | ```html 68 | 81 | ``` 82 | 83 | More on the sidebar [here](https://azouaoui-med.github.io/css-pro-layout/docs/reference/sidebar) 84 | 85 | ### Menu 86 | 87 | Wrapper component that groups all menu items 88 | 89 | ```html 90 | 93 | ``` 94 | 95 | **Open current submenu** 96 | 97 | Use `.open-current-submenu` to enable opening only one submenu component at a time 98 | 99 | ```html 100 | 103 | ``` 104 | 105 | **Icon shape** 106 | 107 | A set of classes are provided to restyle menu icons 108 | 109 | - `.icon-shape-square` 110 | - `.icon-shape-rounded` 111 | - `.icon-shape-circle` 112 | 113 | ```html 114 | 117 | ``` 118 | 119 | ### Menu Item 120 | 121 | Building menu item requires having `.menu-item` class in the wrapper and `.menu-title` for the text 122 | 123 | ```html 124 | 134 | ``` 135 | 136 | **Menu Icon** 137 | 138 | Use `.menu-icon` to add an icon to menu items 139 | 140 | ```html 141 | 154 | ``` 155 | 156 | **Prefix & Suffix** 157 | 158 | Menu item also supports having prefix and suffix components 159 | 160 | ```html 161 | 176 | ``` 177 | 178 | ### Sub Menu 179 | 180 | Add `.sub-menu` class to menu item and create a wrapper component with `sub-menu-list` class to group sub menu items 181 | 182 | > Its possible to have unlimited nesting menu items 183 | 184 | ```html 185 | 203 | ``` 204 | 205 | **Open default** 206 | 207 | Use `.open` class to have sub menu expanded by default 208 | 209 | ```html 210 | 228 | ``` 229 | 230 | ### Customization 231 | 232 | Update SCSS variables in `src/styles/_variables.scss` to customize the template 233 | 234 | ```scss 235 | $text-color: #b3b8d4; 236 | $secondary-text-color: #dee2ec; 237 | $bg-color: #0c1e35; 238 | $secondary-bg-color: #0b1a2c; 239 | $border-color: rgba(#535d7d, 0.3); 240 | $sidebar-header-height: 64px; 241 | $sidebar-footer-height: 64px; 242 | ``` 243 | 244 | ## License 245 | 246 | This code is released under the [MIT](https://github.com/azouaoui-med/pro-sidebar-template/blob/gh-pages/LICENSE) license. 247 | -------------------------------------------------------------------------------- /src/styles/_menu.scss: -------------------------------------------------------------------------------- 1 | @keyframes swing { 2 | 0%, 3 | 30%, 4 | 50%, 5 | 70%, 6 | 100% { 7 | transform: rotate(0deg); 8 | } 9 | 10 | 10% { 11 | transform: rotate(10deg); 12 | } 13 | 14 | 40% { 15 | transform: rotate(-10deg); 16 | } 17 | 18 | 60% { 19 | transform: rotate(5deg); 20 | } 21 | 22 | 80% { 23 | transform: rotate(-5deg); 24 | } 25 | } 26 | 27 | .layout { 28 | .sidebar { 29 | .menu { 30 | ul { 31 | list-style-type: none; 32 | padding: 0; 33 | margin: 0; 34 | } 35 | .menu-header { 36 | font-weight: 600; 37 | padding: 10px 25px; 38 | font-size: 0.8em; 39 | letter-spacing: 2px; 40 | transition: opacity 0.3s; 41 | opacity: 0.5; 42 | } 43 | .menu-item { 44 | a { 45 | display: flex; 46 | align-items: center; 47 | height: 50px; 48 | padding: 0 20px; 49 | color: $text-color; 50 | 51 | .menu-icon { 52 | font-size: 1.2rem; 53 | width: 35px; 54 | min-width: 35px; 55 | height: 35px; 56 | line-height: 35px; 57 | text-align: center; 58 | display: inline-block; 59 | margin-right: 10px; 60 | border-radius: 2px; 61 | transition: color 0.3s; 62 | i { 63 | display: inline-block; 64 | } 65 | } 66 | 67 | .menu-title { 68 | font-size: 0.9em; 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | white-space: nowrap; 72 | flex-grow: 1; 73 | transition: color 0.3s; 74 | } 75 | .menu-prefix, 76 | .menu-suffix { 77 | display: inline-block; 78 | padding: 5px; 79 | opacity: 1; 80 | transition: opacity 0.3s; 81 | } 82 | &:hover { 83 | .menu-title { 84 | color: $secondary-text-color; 85 | } 86 | .menu-icon { 87 | color: $secondary-text-color; 88 | i { 89 | animation: swing ease-in-out 0.5s 1 alternate; 90 | } 91 | } 92 | &::after { 93 | border-color: $secondary-text-color !important; 94 | } 95 | } 96 | } 97 | 98 | &.sub-menu { 99 | position: relative; 100 | > a { 101 | &::after { 102 | content: ''; 103 | transition: transform 0.3s; 104 | border-right: 2px solid currentcolor; 105 | border-bottom: 2px solid currentcolor; 106 | width: 5px; 107 | height: 5px; 108 | transform: rotate(-45deg); 109 | } 110 | } 111 | 112 | > .sub-menu-list { 113 | padding-left: 20px; 114 | display: none; 115 | overflow: hidden; 116 | z-index: 999; 117 | } 118 | &.open { 119 | > a { 120 | color: $secondary-text-color; 121 | &::after { 122 | transform: rotate(45deg); 123 | } 124 | } 125 | } 126 | } 127 | 128 | &.active { 129 | > a { 130 | .menu-title { 131 | color: $secondary-text-color; 132 | } 133 | &::after { 134 | border-color: $secondary-text-color; 135 | } 136 | .menu-icon { 137 | color: $secondary-text-color; 138 | } 139 | } 140 | } 141 | } 142 | > ul > .sub-menu > .sub-menu-list { 143 | background-color: $secondary-bg-color; 144 | } 145 | 146 | &.icon-shape-circle, 147 | &.icon-shape-rounded, 148 | &.icon-shape-square { 149 | .menu-item a .menu-icon { 150 | background-color: $secondary-bg-color; 151 | } 152 | } 153 | 154 | &.icon-shape-circle .menu-item a .menu-icon { 155 | border-radius: 50%; 156 | } 157 | &.icon-shape-rounded .menu-item a .menu-icon { 158 | border-radius: 4px; 159 | } 160 | &.icon-shape-square .menu-item a .menu-icon { 161 | border-radius: 0; 162 | } 163 | } 164 | 165 | &:not(.collapsed) { 166 | .menu > ul { 167 | > .menu-item { 168 | &.sub-menu { 169 | > .sub-menu-list { 170 | visibility: visible !important; 171 | position: static !important; 172 | transform: translate(0, 0) !important; 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | &.collapsed { 180 | .menu > ul { 181 | > .menu-header { 182 | opacity: 0; 183 | } 184 | > .menu-item { 185 | > a { 186 | .menu-prefix, 187 | .menu-suffix { 188 | opacity: 0; 189 | } 190 | } 191 | &.sub-menu { 192 | > a { 193 | &::after { 194 | content: ''; 195 | width: 5px; 196 | height: 5px; 197 | background-color: currentcolor; 198 | border-radius: 50%; 199 | display: inline-block; 200 | position: absolute; 201 | right: 10px; 202 | top: 50%; 203 | border: none; 204 | transform: translateY(-50%); 205 | } 206 | &:hover { 207 | &::after { 208 | background-color: $secondary-text-color; 209 | } 210 | } 211 | } 212 | > .sub-menu-list { 213 | transition: none !important; 214 | width: 200px; 215 | margin-left: 3px !important; 216 | border-radius: 4px; 217 | display: block !important; 218 | } 219 | } 220 | &.active { 221 | > a { 222 | &::after { 223 | background-color: $secondary-text-color; 224 | } 225 | } 226 | } 227 | } 228 | } 229 | } 230 | &.has-bg-image { 231 | .menu { 232 | &.icon-shape-circle, 233 | &.icon-shape-rounded, 234 | &.icon-shape-square { 235 | .menu-item a .menu-icon { 236 | background-color: rgba($secondary-bg-color, 0.6); 237 | } 238 | } 239 | } 240 | &:not(.collapsed) { 241 | .menu { 242 | > ul > .sub-menu > .sub-menu-list { 243 | background-color: rgba($secondary-bg-color, 0.6); 244 | } 245 | } 246 | } 247 | } 248 | } 249 | 250 | &.rtl { 251 | .sidebar { 252 | .menu { 253 | .menu-item { 254 | a { 255 | .menu-icon { 256 | margin-left: 10px; 257 | margin-right: 0; 258 | } 259 | } 260 | 261 | &.sub-menu { 262 | > a { 263 | &::after { 264 | transform: rotate(135deg); 265 | } 266 | } 267 | 268 | > .sub-menu-list { 269 | padding-left: 0; 270 | padding-right: 20px; 271 | } 272 | &.open { 273 | > a { 274 | &::after { 275 | transform: rotate(45deg); 276 | } 277 | } 278 | } 279 | } 280 | } 281 | } 282 | 283 | &.collapsed { 284 | .menu > ul { 285 | > .menu-item { 286 | &.sub-menu { 287 | a::after { 288 | right: auto; 289 | left: 10px; 290 | } 291 | 292 | > .sub-menu-list { 293 | margin-left: -3px !important; 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /public/assets/remixicon/remixicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | Pro Sidebar 13 | 14 | 15 |
16 | 269 |
270 |
271 |
272 |
273 | 274 | 275 | 276 |

Pro Sidebar

277 | 278 | Responsive layout with advanced sidebar menu built with SCSS and vanilla Javascript 279 | 280 | 294 |
295 |
296 |

Features

297 |
    298 |
  • Fully responsive
  • 299 |
  • Collapsable sidebar
  • 300 |
  • Multi level menu
  • 301 |
  • RTL support
  • 302 |
  • Customizable
  • 303 |
304 |
305 |
306 |

Resources

307 | 320 |
321 | 343 |
344 |
345 |
346 |
347 | 348 | 349 | 350 | --------------------------------------------------------------------------------