Add sidebar user block with pinned footer zone

- three-zone sidebar: pinned brand, scrollable nav, navbar-footer with a user dropup demo
- titles never wrap (nowrap + ellipsis) and icons/avatar keep size during the fold animation
- btn-floating moved to the end side so it does not cover the sidebar
This commit is contained in:
codecalm
2026-08-21 01:19:01 +02:00
parent 75fc2d3eef
commit d1a4d89e63
12 changed files with 208 additions and 72 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/core": patch
---
Moved `btn-floating` to the end side of the screen, so it does not cover the sidebar.
+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": minor
"@tabler/preview": minor
"@tabler/docs": minor
---
Added a `navbar-footer` sidebar zone with a user block; the sidebar nav scrolls between the pinned brand and footer.
+57 -14
View File
@@ -3,15 +3,8 @@
@use '../config' as *;
@mixin navbar-vertical-nav {
.navbar-collapse {
flex-direction: column;
[class^='container'] {
flex-direction: column;
align-items: stretch;
padding: 0;
}
// Applies to every navbar-nav in the vertical navbar — the menu inside
// .navbar-collapse and e.g. a .navbar-footer list outside of it.
.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
@@ -38,6 +31,15 @@
padding: var(--sidebar-nav-padding-y) var(--sidebar-nav-padding-x);
}
// One line, ellipsized — a title never wraps, so rows keep their height
// while the folding sidebar animates its width.
.nav-link-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// stylelint-disable-next-line selector-max-class
.nav-item.active > .nav-link {
--nav-link-icon-color: currentColor;
@@ -62,6 +64,15 @@
}
}
.navbar-collapse {
flex-direction: column;
[class^='container'] {
flex-direction: column;
align-items: stretch;
padding: 0;
}
.dropdown-menu-columns {
flex-direction: column;
}
@@ -146,9 +157,10 @@
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.
// icons keep their exact position — only the titles collapse. Aside-wide
// (not .navbar-collapse-scoped), so the footer outside the collapse folds
// the same way.
.navbar-nav .nav-link {
--nav-link-icon-margin-end: 0;
}
@@ -161,6 +173,11 @@
opacity: 0;
}
.navbar-collapse {
// Flyouts must escape the nav's scroll area; the folded rail therefore
// cannot scroll (documented — the hover variant is the escape hatch).
overflow: visible;
// The label text cannot fit the rail — indent it away (stays in the
// accessibility tree, line box keeps the element height stable) and show
// a short separator instead, centered within the WHOLE gap between the
@@ -537,7 +554,6 @@ Navbar vertical
align-items: start;
width: var(--sidebar-width);
padding: 0;
overflow-y: scroll;
@include transition(transform $transition-time);
&.navbar-right,
@@ -570,8 +586,12 @@ Navbar vertical
opacity: 1;
}
// The nav is the sidebar's scroll area — the brand above and the
// footer below stay pinned.
.navbar-collapse {
align-items: stretch;
min-height: 0;
overflow-y: auto;
}
.navbar-nav {
@@ -585,11 +605,35 @@ Navbar vertical
}
}
// Bottom-pinned footer area, e.g. a user block. Sits after the
// growing nav, separated with the navbar border.
.navbar-footer {
padding-block: $sidebar-inset;
margin-top: auto;
border-top: var(--navbar-border-width) var(--border-style) var(--navbar-border-color);
// Tighter link padding than the menu — the avatar brings its
// own bulk. Overrides the variables the nav-link padding reads,
// so it wins regardless of rule order.
.navbar-nav {
--sidebar-nav-padding-y: 0.25rem;
--sidebar-nav-padding-x: 0.25rem;
flex-grow: 0;
}
.nav-link .avatar {
// Never squeezed by the appearing title during the hover
// unfold animation.
flex-shrink: 0;
margin-inline-end: var(--nav-link-icon-margin-end);
}
}
> [class^='container'] {
flex-direction: column;
align-items: stretch;
justify-content: start;
min-height: 100%;
height: 100%;
padding: 0;
}
@@ -633,7 +677,6 @@ Navbar vertical
&:hover,
&:has(:focus-visible) {
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);
+2 -1
View File
@@ -223,7 +223,8 @@
.btn-floating {
--btn-floating-offset: #{$btn-floating-offset};
position: fixed;
inset-inline-start: var(--btn-floating-offset);
// End side, Material-FAB style — the start side belongs to the sidebar.
inset-inline-end: var(--btn-floating-offset);
bottom: var(--btn-floating-offset);
z-index: $zindex-fixed;
box-shadow: var(--shadow-dropdown);
+1
View File
@@ -97,6 +97,7 @@
}
.nav-link-icon {
flex-shrink: 0;
width: var(--nav-link-icon-width, #{$nav-link-icon-size});
height: var(--nav-link-icon-height, #{$nav-link-icon-size});
margin-inline-end: var(--nav-link-icon-margin-end);
+32
View File
@@ -242,6 +242,38 @@ Group related menu items with a label. Add a `nav-section-title` list item befor
The label is aligned with the item text and muted, so it does not compete with the links. In the folded sidebar, the label text is replaced with a short separator line centered in the gap between the groups, and the label keeps its height, so the menu does not shift when the sidebar folds. Section labels render only in vertical navbars — a horizontal navbar hides them.
### Sidebar footer and user block
The vertical sidebar has three zones: the brand at the top, the nav as its own scroll area, and an optional `navbar-footer` pinned at the bottom. A common use is a user block with a drop-up menu:
```html
<aside class="navbar navbar-vertical navbar-expand-lg">
<div class="container-fluid">
<div class="navbar-brand">...</div>
<nav class="collapse navbar-collapse" id="sidebar-menu">...</nav>
<div class="navbar-footer">
<ul class="navbar-nav">
<li class="nav-item dropup">
<a href="#" class="nav-link" data-bs-toggle="dropdown" aria-label="Open user menu">
<span class="avatar" style="background-image: url(...)"></span>
<span class="nav-link-title">
Paweł Kuna
<div class="small text-secondary">UI Designer</div>
</span>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#">Profile</a>
<a class="dropdown-item" href="#">Sign out</a>
</div>
</li>
</ul>
</div>
</div>
</aside>
```
The nav scrolls independently, so the footer stays visible with long menus. In the folded sidebar, the name and role collapse like link titles and the avatar is centered in the rail.
## Folded sidebar
Add `navbar-folded` to a vertical navbar to fold it into a narrow, icon-only rail. Link titles are hidden, icons are centered and submenus open as floating flyout menus next to the rail. The page content moves over automatically, so you get more space for your app.
+1 -1
View File
@@ -4,7 +4,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
---
<DefaultLayout title="Dashboard" pageHeader="Folded hover layout" pretitle="Overview" pageMenu="layout.folded-hover" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFoldedHover hideTopbar>
<DefaultLayout title="Dashboard" pageHeader="Folded hover layout" pretitle="Overview" pageMenu="layout.folded-hover" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFoldedHover sidebarUser hideTopbar>
<HeaderActionsButtons slot="page-header-actions" />
<HomepageContent />
</DefaultLayout>
+1 -1
View File
@@ -4,7 +4,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
---
<DefaultLayout title="Dashboard" pageHeader="Folded sidebar layout" pretitle="Overview" pageMenu="layout.folded" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFolded hideTopbar>
<DefaultLayout title="Dashboard" pageHeader="Folded sidebar layout" pretitle="Overview" pageMenu="layout.folded" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFolded sidebarUser hideTopbar>
<HeaderActionsButtons slot="page-header-actions" />
<HomepageContent />
</DefaultLayout>
+1 -1
View File
@@ -6,7 +6,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
---
<DefaultLayout title="Dashboard" pageHeader="Vertical layout" pretitle="Overview" pageLibs={['apexcharts', 'jsvectormap']} pageMenu="layout.vertical" sidebar sidebarFoldToggle hideTopbar>
<DefaultLayout title="Dashboard" pageHeader="Vertical layout" pretitle="Overview" pageLibs={['apexcharts', 'jsvectormap']} pageMenu="layout.vertical" sidebar sidebarFoldToggle sidebarUser hideTopbar>
<HeaderActionsButtons slot="page-header-actions" />
<HomepageContent />
</DefaultLayout>
+6 -1
View File
@@ -3,6 +3,7 @@ import NavbarToggler from './NavbarToggler.astro'
import Logo from './Logo.astro'
import NavbarSide from './NavbarSide.astro'
import NavbarMenu from './NavbarMenu.astro'
import SidebarUser from './SidebarUser.astro'
import Icon from '@ui/Icon.astro'
import type { Breakpoint } from './NavbarSide.astro'
@@ -19,11 +20,13 @@ interface Props {
foldedHover?: boolean | undefined
/** renders a bottom-pinned button toggling the folded state at runtime */
foldToggle?: boolean | undefined
/** renders a user block in the sidebar footer */
user?: boolean | undefined
/** equivalent of the page-menu front matter (active menu entry), e.g. "layout.vertical" */
pageMenu?: string | undefined
}
const { dark = false, breakpoint = 'lg', end = false, transparent = false, folded = false, foldedHover = false, foldToggle = false, pageMenu } = Astro.props
const { dark = false, breakpoint = 'lg', end = false, transparent = false, folded = false, foldedHover = false, foldToggle = false, user = false, pageMenu } = Astro.props
const isFolded = folded || foldedHover
@@ -53,6 +56,8 @@ const asideClass = ['navbar navbar-vertical', end && 'navbar-end', `navbar-expan
{/* A folded sidebar renders flyout submenus, so the active branch is not kept open inline. */}
<NavbarMenu autoOpen={!isFolded} class="pt-lg-3" keepOpen={!isFolded} sections pageMenu={pageMenu} />
</nav>
{user && <SidebarUser />}
</div>
</aside>
<!-- END SIDEBAR -->
@@ -0,0 +1,39 @@
---
// person-id=1 → people[0] (1-based index).
import { people, personById } from '@shared/lib/people'
import Icon from '@ui/Icon.astro'
import Avatar from '@ui/Avatar.astro'
import { requireIndex } from '@shared/lib/array'
interface Props {
personId?: number
}
const { personId = 1 } = Astro.props
const person = requireIndex(people, personId - 1)
---
<!-- BEGIN SIDEBAR USER -->
<div class="navbar-footer d-none d-lg-block">
<ul class="navbar-nav">
<li class="nav-item dropup">
<a href="#" class="nav-link" data-bs-toggle="dropdown" aria-label="Open user menu">
<Avatar person={personById(personId)} />
<span class="nav-link-title">
{person.full_name}
<div class="small text-secondary">{person.job_title}</div>
</span>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="./profile.html"><Icon name="user" class="dropdown-item-icon" /> Profile</a>
<a class="dropdown-item" href="#"><Icon name="chart-pie" class="dropdown-item-icon" /> Analytics</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="./settings.html">Settings &amp; Privacy</a>
<a class="dropdown-item" href="#">Help</a>
<a class="dropdown-item" href="./sign-in.html">Sign out</a>
</div>
</li>
</ul>
</div>
<!-- END SIDEBAR USER -->
+4 -1
View File
@@ -44,6 +44,8 @@ interface Props {
sidebarFoldedHover?: boolean
/** renders the runtime fold toggle button in the sidebar */
sidebarFoldToggle?: boolean
/** layout-sidebar-user — user block in the sidebar footer */
sidebarUser?: boolean
/** layout-navbar-transparent (sidebar variant) */
navbarTransparent?: boolean
/** layout-navbar-condensed */
@@ -80,6 +82,7 @@ const {
sidebarFolded,
sidebarFoldedHover,
sidebarFoldToggle,
sidebarUser,
navbarTransparent,
navbarCondensed,
navbarDark,
@@ -100,7 +103,7 @@ const headerTitle = pageHeader === false ? undefined : (pageHeader ?? title)
sidebar && (
<Fragment>
<Fragment set:html={'<!-- BEGIN SIDEBAR -->'} />
<Sidebar dark={sidebarDark} end={sidebarEnd} transparent={navbarTransparent} folded={sidebarFolded} foldedHover={sidebarFoldedHover} foldToggle={sidebarFoldToggle} breakpoint="lg" pageMenu={pageMenu} />
<Sidebar dark={sidebarDark} end={sidebarEnd} transparent={navbarTransparent} folded={sidebarFolded} foldedHover={sidebarFoldedHover} foldToggle={sidebarFoldToggle} user={sidebarUser} breakpoint="lg" pageMenu={pageMenu} />
<Fragment set:html={'<!-- END SIDEBAR -->'} />
</Fragment>
)