diff --git a/.changeset/btn-floating-end.md b/.changeset/btn-floating-end.md
new file mode 100644
index 000000000..2fb8f1c00
--- /dev/null
+++ b/.changeset/btn-floating-end.md
@@ -0,0 +1,5 @@
+---
+"@tabler/core": patch
+---
+
+Moved `btn-floating` to the end side of the screen, so it does not cover the sidebar.
diff --git a/.changeset/sidebar-user-footer.md b/.changeset/sidebar-user-footer.md
new file mode 100644
index 000000000..a3bad8fdc
--- /dev/null
+++ b/.changeset/sidebar-user-footer.md
@@ -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.
diff --git a/core/scss/layout/_navbar.scss b/core/scss/layout/_navbar.scss
index e3a9cd28f..5a8cd175b 100644
--- a/core/scss/layout/_navbar.scss
+++ b/core/scss/layout/_navbar.scss
@@ -3,6 +3,67 @@
@use '../config' as *;
@mixin navbar-vertical-nav {
+ // 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
+ // 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)};
+ --nav-link-icon-width: #{$sidebar-nav-icon-width};
+ --nav-link-icon-height: #{$sidebar-nav-icon-height};
+ --sidebar-inset: #{$sidebar-inset};
+ --sidebar-nav-padding-y: #{$sidebar-nav-padding-y};
+ --sidebar-nav-padding-x: #{$sidebar-nav-padding-x};
+ --sidebar-nav-indent: #{$sidebar-nav-indent};
+ --sidebar-section-title-font-size: #{$sidebar-section-title-font-size};
+ --sidebar-section-title-margin-top: #{$sidebar-section-title-margin-top};
+ // 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: var(--sidebar-inset);
+ margin-inline-start: 0;
+ margin-inline-end: 0;
+
+ .nav-link {
+ justify-content: start;
+ 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;
+ font-weight: var(--font-weight-medium);
+ background: var(--navbar-active-bg);
+ }
+
+ // Group label — aligned with the item text, muted like the icons.
+ .nav-section-title {
+ display: block;
+ padding: 0.25rem var(--sidebar-nav-padding-x);
+ margin-top: var(--sidebar-section-title-margin-top);
+ font-size: var(--sidebar-section-title-font-size);
+ font-weight: var(--font-weight-medium);
+ color: #{color-transparent(var(--navbar-color), 0.7)};
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+ }
+
.navbar-collapse {
flex-direction: column;
@@ -12,56 +73,6 @@
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)};
- --nav-link-icon-width: #{$sidebar-nav-icon-width};
- --nav-link-icon-height: #{$sidebar-nav-icon-height};
- --sidebar-inset: #{$sidebar-inset};
- --sidebar-nav-padding-y: #{$sidebar-nav-padding-y};
- --sidebar-nav-padding-x: #{$sidebar-nav-padding-x};
- --sidebar-nav-indent: #{$sidebar-nav-indent};
- --sidebar-section-title-font-size: #{$sidebar-section-title-font-size};
- --sidebar-section-title-margin-top: #{$sidebar-section-title-margin-top};
- // 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: var(--sidebar-inset);
- margin-inline-start: 0;
- margin-inline-end: 0;
-
- .nav-link {
- justify-content: start;
- padding: var(--sidebar-nav-padding-y) var(--sidebar-nav-padding-x);
- }
-
- // 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);
- }
-
- // Group label — aligned with the item text, muted like the icons.
- .nav-section-title {
- display: block;
- padding: 0.25rem var(--sidebar-nav-padding-x);
- margin-top: var(--sidebar-section-title-margin-top);
- font-size: var(--sidebar-section-title-font-size);
- font-weight: var(--font-weight-medium);
- color: #{color-transparent(var(--navbar-color), 0.7)};
- text-transform: uppercase;
- letter-spacing: 0.04em;
-
- &:first-child {
- margin-top: 0;
- }
- }
- }
-
.dropdown-menu-columns {
flex-direction: column;
}
@@ -146,20 +157,26 @@
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;
- }
+ // The list inset and link padding are identical in both states, so the
+ // 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;
+ }
- // Collapsed, not display: none — titles stay in the accessibility tree.
- .nav-link-title {
- width: 0;
- overflow: hidden;
- white-space: nowrap;
- opacity: 0;
- }
+ // Collapsed, not display: none — titles stay in the accessibility tree.
+ .nav-link-title {
+ width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ 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
@@ -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);
diff --git a/core/scss/ui/_buttons.scss b/core/scss/ui/_buttons.scss
index 2310abf20..a24ca8b25 100644
--- a/core/scss/ui/_buttons.scss
+++ b/core/scss/ui/_buttons.scss
@@ -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);
diff --git a/core/scss/ui/_nav.scss b/core/scss/ui/_nav.scss
index e025fe61c..e05d62a48 100644
--- a/core/scss/ui/_nav.scss
+++ b/core/scss/ui/_nav.scss
@@ -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);
diff --git a/docs/content/ui/layout/page-layouts.mdx b/docs/content/ui/layout/page-layouts.mdx
index ba8716ead..ac414f807 100644
--- a/docs/content/ui/layout/page-layouts.mdx
+++ b/docs/content/ui/layout/page-layouts.mdx
@@ -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
+
+```
+
+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.
diff --git a/preview/pages/layout-folded-hover.astro b/preview/pages/layout-folded-hover.astro
index c31ecfc20..fa3a9e4b5 100644
--- a/preview/pages/layout-folded-hover.astro
+++ b/preview/pages/layout-folded-hover.astro
@@ -4,7 +4,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
---
-
+
diff --git a/preview/pages/layout-folded.astro b/preview/pages/layout-folded.astro
index ebfbf9387..b357f8b03 100644
--- a/preview/pages/layout-folded.astro
+++ b/preview/pages/layout-folded.astro
@@ -4,7 +4,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
---
-
+
diff --git a/preview/pages/layout-vertical.astro b/preview/pages/layout-vertical.astro
index 69b00a424..e0da80e8f 100644
--- a/preview/pages/layout-vertical.astro
+++ b/preview/pages/layout-vertical.astro
@@ -6,7 +6,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
---
-
+
diff --git a/shared/components/navbar/Sidebar.astro b/shared/components/navbar/Sidebar.astro
index 78fb432a6..9c5c3c0e2 100644
--- a/shared/components/navbar/Sidebar.astro
+++ b/shared/components/navbar/Sidebar.astro
@@ -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. */}
+
+ {user && }
diff --git a/shared/components/navbar/SidebarUser.astro b/shared/components/navbar/SidebarUser.astro
new file mode 100644
index 000000000..af64b446d
--- /dev/null
+++ b/shared/components/navbar/SidebarUser.astro
@@ -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)
+---
+
+
+
+
diff --git a/shared/layouts/DefaultLayout.astro b/shared/layouts/DefaultLayout.astro
index 6977df422..1ef643da0 100644
--- a/shared/layouts/DefaultLayout.astro
+++ b/shared/layouts/DefaultLayout.astro
@@ -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 && (
'} />
-
+
'} />
)