Files
tabler/core/scss/layout/_navbar.scss
T
codecalm 6481499ccd Add folded sidebar with flyout menus, pin toggle and light default
- navbar-folded / navbar-folded-hover with flyout submenus and persisted pin toggle
- sidebar refresh: light by default, 16rem wide, rounded inset pills, zero-jump fold
- demo pages, Theme Settings section, docs and btn-action usage rule
2026-08-20 22:27:53 +02:00

595 lines
16 KiB
SCSS

@use 'sass:map';
@use '../config' as *;
@mixin navbar-vertical-nav {
.navbar-collapse {
flex-direction: column;
[class^='container'] {
flex-direction: column;
align-items: stretch;
padding: 0;
}
.navbar-nav {
// Sidebar icons carry more meaning than inline tab icons, so they get
// stronger contrast than the global 50% $nav-link-icon-color. Derived
// from --navbar-color, which is defined at this scope (unlike
// --nav-link-color, which only exists inside .nav).
--nav-link-icon-color: #{color-transparent(var(--navbar-color), 0.7)};
// Inset the list so the rounded hover/active pills never touch the
// sidebar edges. The same inset and link padding apply in the folded
// state, so icons keep their exact position when the sidebar folds.
padding-inline: $sidebar-inset;
margin-inline-start: 0;
margin-inline-end: 0;
.nav-link {
justify-content: start;
padding: 0.75rem 0.75rem;
}
// stylelint-disable-next-line selector-max-class
.nav-item.active > .nav-link {
--nav-link-icon-color: currentColor;
font-weight: var(--font-weight-medium);
background: var(--navbar-active-bg);
}
}
.dropdown-menu-columns {
flex-direction: column;
}
.dropdown-menu {
position: static;
min-width: 0;
padding: 0;
margin: 0;
color: inherit;
background: transparent;
border: none;
box-shadow: none;
.dropdown-item {
display: flex;
width: auto;
min-width: 0;
// Link padding (0.75rem) + the icon column (1.75rem) — items align
// with the parent link's title.
padding-inline-start: 2.5rem;
color: inherit;
@include border-radius(var(--border-radius));
&.disabled {
color: var(--disabled-color);
pointer-events: none;
background-color: transparent;
}
&.active,
&:active {
font-weight: var(--font-weight-medium);
background: var(--navbar-active-bg);
}
}
.dropdown-menu .dropdown-item {
padding-inline-start: 4rem;
}
// Third-level submenu in the mobile vertical navbar — genuinely nested two
// dropdown-menu levels deep.
// stylelint-disable-next-line selector-max-class, selector-max-compound-selectors
.dropdown-menu .dropdown-menu .dropdown-item {
padding-inline-start: 5.5rem;
}
}
.dropdown-toggle::after {
margin-inline-start: auto;
}
// The active pill is the selection indicator — no underline or edge bar.
.nav-item.active::after {
content: none;
}
}
}
// Icon-only rail: hides link titles, centers icons and turns the inline submenus
// flattened by navbar-vertical-nav() back into floating flyout dropdowns.
@mixin navbar-vertical-folded {
// Flyouts must escape the rail, so the folded sidebar itself cannot scroll.
overflow: visible;
// A wide brand (e.g. a full logo) must not push the container beyond the
// rail width; the brand crops instead.
[class^='container'] {
min-width: 0;
}
// The brand keeps the expanded state's start padding, so the logo does not
// move when the sidebar folds; anything wider than the rail crops.
.navbar-brand {
overflow: hidden;
}
// The pin button lives on the expanded sidebar only; the folded rail
// unfolds by hovering (folded-hover) or via an outside control.
[data-bs-toggle='sidebar-folded'] {
display: none;
}
.navbar-collapse {
// The list inset and link padding are identical in both states, so the
// icons keep their exact position — only the titles collapse.
.navbar-nav .nav-link {
--nav-link-icon-margin-end: 0;
}
// Collapsed, not display: none — titles stay in the accessibility tree.
.nav-link-title {
width: 0;
overflow: hidden;
white-space: nowrap;
opacity: 0;
}
.nav-link.dropdown-toggle::after {
display: none;
}
// Restore the multi-column layout that navbar-vertical-nav() stacks —
// it keeps long flyout menus from overflowing the viewport.
.dropdown-menu-columns {
flex-direction: row;
}
.dropdown-menu {
position: absolute;
inset-inline-start: 100%;
top: 0;
min-width: var(--dropdown-min-width);
padding: var(--dropdown-padding-y) var(--dropdown-padding-x);
margin: 0;
color: var(--dropdown-color);
background-color: var(--dropdown-bg);
background-clip: padding-box;
// No border — like the base .dropdown-menu, whose edge is the 1px ring
// built into --shadow-dropdown.
box-shadow: var(--dropdown-box-shadow);
@include border-radius(var(--dropdown-border-radius));
.dropdown-item {
padding: var(--dropdown-item-padding-y) var(--dropdown-item-padding-x);
color: var(--dropdown-link-color);
@include border-radius(0);
}
// Undo the tree indents navbar-vertical-nav() puts on nested levels.
.dropdown-menu .dropdown-item {
padding-inline-start: var(--dropdown-item-padding-x);
}
// stylelint-disable-next-line selector-max-class, selector-max-compound-selectors
.dropdown-menu .dropdown-menu .dropdown-item {
padding-inline-start: var(--dropdown-item-padding-x);
}
}
// The nav list is inset, so 100% of the .dropdown lands short of the rail
// edge — compensate for the inset and add a small gap, so the flyout's
// border does not sit flush against the rail's edge border. Nested
// (dropend) flyouts are not inset and keep the plain 100%.
.nav-item > .dropdown-menu {
inset-inline-start: calc(100% + #{$sidebar-inset + 0.25rem});
}
}
}
/**
Navbar
*/
.navbar {
--navbar-bg: var(--bg-surface);
--navbar-border-width: #{$navbar-border-width};
--navbar-active-border-color: #{$navbar-active-border-color};
--navbar-active-bg: #{$navbar-light-active-bg};
--navbar-border-color: #{$navbar-border-color};
--navbar-hover-color: #{$navbar-hover-color};
align-items: stretch;
min-height: $navbar-height;
color: var(--navbar-color);
background: var(--navbar-bg);
box-shadow: inset 0 calc(-1 * var(--navbar-border-width)) 0 0 var(--navbar-border-color);
@include media-print() {
display: none;
}
.navbar-collapse & {
flex-grow: 1;
}
&.collapsing {
min-height: 0;
}
.dropdown-menu {
position: absolute;
z-index: $zindex-fixed;
}
.navbar-nav {
--nav-link-icon-margin-end: #{$nav-link-icon-margin-end};
min-height: subtract($navbar-height, 2 * $navbar-padding-y);
.nav-link {
position: relative;
justify-content: center;
min-width: 2.5rem;
min-height: 2.5rem;
@include border-radius(var(--border-radius));
.badge {
position: absolute;
inset-inline-end: 0.5rem;
top: 0.5rem;
/* rtl:ignore */
transform: translate(calc(var(--dir) * 50%), -50%);
}
}
}
}
.navbar-expand {
@each $breakpoint in map.keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
&#{$infix} {
@include media-breakpoint-down(breakpoint-next($breakpoint)) {
@include navbar-vertical-nav();
}
@include media-breakpoint-up($next) {
.navbar-collapse {
flex: 1 1 auto;
width: auto;
}
.nav-item.active {
position: relative;
.nav-link {
color: var(--navbar-active-color);
}
&::after {
position: absolute;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: -0.25rem;
content: '';
border: 0 var(--border-style) var(--navbar-active-border-color);
border-bottom-width: 2px;
}
}
&.navbar-vertical {
box-shadow: inset calc(-1 * var(--navbar-border-width)) 0 0 0 var(--navbar-border-color);
&.navbar-right,
&.navbar-end {
box-shadow: inset calc(1 * var(--navbar-border-width)) 0 0 0 var(--navbar-border-color);
}
}
&.navbar-vertical {
~ .navbar,
~ .page-wrapper {
margin-inline-start: var(--sidebar-width);
}
}
&.navbar-vertical.navbar-right,
&.navbar-vertical.navbar-end {
~ .navbar,
~ .page-wrapper {
margin-inline-start: 0;
margin-inline-end: var(--sidebar-width);
}
}
}
}
}
}
/**
Navbar brand
*/
.navbar-brand {
display: inline-flex;
gap: $spacer-2;
align-items: center;
margin: 0;
font-weight: $navbar-brand-font-weight;
line-height: 1;
}
.navbar-brand-image {
width: auto;
height: $navbar-brand-image-height;
}
.navbar-brand-gray {
--navbar-logo-color: var(--tertiary);
opacity: 0.5;
}
/**
Navbar toggler
*/
.navbar-toggler {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: $navbar-brand-image-height;
height: $navbar-brand-image-height;
border: 0;
}
.navbar-toggler-icon {
position: relative;
width: 1.25em;
height: 2px;
background: currentColor;
@include border-radius(var(--border-radius-pill));
@include transition(top $navbar-toggler-animation-time $navbar-toggler-animation-time, bottom $navbar-toggler-animation-time $navbar-toggler-animation-time, transform $navbar-toggler-animation-time, opacity 0s $navbar-toggler-animation-time);
&::before,
&::after {
position: absolute;
inset-inline-start: 0;
display: block;
width: inherit;
height: inherit;
content: '';
background: inherit;
border-radius: inherit;
@include transition(inherit);
}
&::before {
top: -0.45em;
}
&::after {
bottom: -0.45em;
}
.navbar-toggler[aria-expanded='true'] & {
transform: rotate(45deg);
@include transition(top $transition-time, bottom $transition-time, transform $transition-time $transition-time, opacity 0s $transition-time);
&::before {
top: 0;
transform: rotate(-90deg);
}
&::after {
bottom: 0;
opacity: 0;
}
}
}
/**
Navbar transparent
*/
.navbar-transparent {
--navbar-border-color: transparent !important;
background: transparent !important;
}
/**
Navbar nav
*/
.navbar-nav {
--nav-link-hover-bg: #{$navbar-nav-link-hover-bg};
--nav-link-icon-color: #{$nav-link-icon-color};
--nav-link-hover-icon-color: #{$nav-link-hover-icon-color};
align-items: stretch;
padding: 0;
margin: 0;
.nav-item {
display: flex;
flex-direction: column;
justify-content: center;
}
}
/**
Navbar side
*/
.navbar-side {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
margin: 0;
}
/**
Navbar vertical
*/
@if $enable-navbar-vertical {
// Folded sidebar: every geometry consumer (aside width, page paddings,
// page-wrapper/navbar margins) reads --sidebar-width, so flipping this one
// variable re-lays-out the whole page. Two routes to the same state:
// a .navbar-folded/.navbar-folded-hover class on the aside (static markup),
// or data-bs-sidebar="folded"/"folded-hover" on <html> (set pre-render from
// localStorage by tabler-theme.js, toggled by [data-bs-toggle="sidebar-folded"]).
// html[…] beats the :root declaration of --sidebar-width on the same element;
// a bare :where()/[…] selector would lose to it and never take effect.
html[data-bs-sidebar^='folded'] {
--sidebar-width: var(--sidebar-folded-width);
}
.navbar-vertical:is(.navbar-folded, .navbar-folded-hover) {
--sidebar-width: var(--sidebar-folded-width);
// The class sits on the aside, so its layout siblings need their own copy.
~ .page,
~ .page-wrapper,
~ .navbar {
--sidebar-width: var(--sidebar-folded-width);
}
}
.navbar-vertical {
&.navbar-expand {
@each $breakpoint in map.keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
&#{$infix} {
@include media-breakpoint-up($next) {
position: fixed;
inset-inline-start: 0;
top: 0;
bottom: 0;
z-index: $zindex-fixed;
align-items: start;
width: var(--sidebar-width);
padding: 0;
overflow-y: scroll;
@include transition(transform $transition-time);
&.navbar-right,
&.navbar-end {
inset-inline-start: auto;
inset-inline-end: 0;
}
// Logo on the start side, room for an action (e.g. the pin
// toggle) on the end side. The inline padding centers the logo
// mark in the folded rail and is identical in both states, so the
// logo mark and the nav icons share one center axis and nothing
// moves when the sidebar folds.
.navbar-brand {
justify-content: space-between;
padding: (($navbar-height - $navbar-brand-image-height) * 0.5) (($sidebar-folded-width - $navbar-brand-image-height) * 0.5);
margin-inline-end: 0;
}
// The fold toggle keeps its layout slot (no jumping) but only
// becomes visible while the sidebar is hovered or focused.
[data-bs-toggle='sidebar-folded'] {
opacity: 0;
@include transition(opacity $transition-time);
}
&:is(:hover, :focus-within) [data-bs-toggle='sidebar-folded'] {
opacity: 1;
}
.navbar-collapse {
align-items: stretch;
}
.navbar-nav {
flex-grow: 1;
flex-direction: column;
min-height: auto;
.nav-link {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
}
> [class^='container'] {
flex-direction: column;
align-items: stretch;
justify-content: start;
min-height: 100%;
padding: 0;
}
~ .page {
padding-inline-start: var(--sidebar-width);
[class^='container'] {
padding-inline-start: 1.5rem;
padding-inline-end: 1.5rem;
}
}
&.navbar-right ~ .page,
&.navbar-end ~ .page {
padding-inline-start: 0;
padding-inline-end: var(--sidebar-width);
}
@include navbar-vertical-nav();
// Folded state, both routes; the hover variant folds only while
// neither hovered nor focused from the keyboard.
&:is(.navbar-folded, .navbar-folded-hover:not(:hover, :focus-within)),
:where([data-bs-sidebar='folded']) &,
:where([data-bs-sidebar='folded-hover']) &:not(:hover, :focus-within) {
@include navbar-vertical-folded();
}
&.navbar-folded-hover,
:where([data-bs-sidebar='folded-hover']) & {
@include transition(transform $transition-time, width $transition-time);
.nav-link-title {
@include transition(opacity $transition-time);
}
// Temporarily unfolds as an overlay: siblings keep the folded
// --sidebar-width, so page content does not reflow.
&:is(:hover, :focus-within) {
width: $sidebar-width;
overflow-y: auto;
box-shadow:
inset calc(-1 * var(--navbar-border-width)) 0 0 0 var(--navbar-border-color),
var(--shadow-lg);
}
}
}
}
}
}
}
@include media-print() {
.navbar-vertical ~ .page {
padding-inline-start: 0;
padding-inline-end: 0;
}
}
}
.navbar-overlap {
&::after {
position: absolute;
inset-inline-start: 0;
inset-inline-end: 0;
top: 100%;
z-index: -1;
height: $navbar-overlap-height;
content: '';
background: inherit;
box-shadow: inherit;
}
}