Layouts
Sidebar
Use a sidebar navigation layout to keep your application content front and center.
Drawn and behaving as Flux UI's sidebar does, measured against its live docs.
Sidebar
Docked beside the main on a desktop. On a phone the header's toggle slides it over the page.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SidebarLayoutExample : Component
{
// The page's body: whatever holds Ui.Main is the layout grid, and the three are siblings in it.
protected override Component? Render() =>
Div.Class("min-h-screen bg-white antialiased dark:bg-zinc-800")[
Navigation(),
Bar(),
Ui.Main[
Ui.Heading.Level(1).Xl["Good afternoon, Mara"],
Ui.Text.Class("mt-2 mb-6 text-base")["Here's what's new today"],
Ui.Separator.Subtle
]
];
private static Component Navigation() =>
Ui.Sidebar.Sticky(true).Collapsible(Ui.SidebarCollapsible.Mobile)
.Class("border-r border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900")[
Ui.SidebarHeader[
Ui.SidebarBrand.Href("#").Logo("/img/rask-mark.svg").Name("Acme Inc."),
Ui.SidebarCollapse.Class("lg:hidden")
],
Ui.SidebarSearch.Placeholder("Search..."),
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Home).Href("#").Current(true)["Home"],
Ui.SidebarItem.Icon(Ui.IconName.Inbox).Badge("12").Href("#")["Inbox"],
Ui.SidebarItem.Icon(Ui.IconName.DocumentText).Href("#")["Documents"],
Ui.SidebarItem.Icon(Ui.IconName.Calendar).Href("#")["Calendar"],
Ui.SidebarGroup.Expandable(true).Heading("Favorites")[
Ui.SidebarItem.Href("#")["Marketing site"],
Ui.SidebarItem.Href("#")["Android app"],
Ui.SidebarItem.Href("#")["Brand guidelines"]
]
],
Ui.SidebarSpacer,
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Cog6Tooth).Href("#")["Settings"],
Ui.SidebarItem.Icon(Ui.IconName.InformationCircle).Href("#")["Help"]
],
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start).Class("flex max-lg:hidden")[
Ui.SidebarProfile.Avatar("/img/avatars/amber.svg").Name("Mara Lind"),
Account()
]
];
// A phone's bar: the toggle that slides the sidebar in, and the account.
private static Component Bar() =>
Ui.Header.Class("lg:hidden")[
Ui.SidebarToggle.Icon(Ui.IconName.Bars2).Inset(Ui.Position.Left),
Ui.Spacer,
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start)[
Ui.Profile.Avatar("/img/avatars/amber.svg"),
Account()
]
];
private static Component Account() =>
Ui.Menu[
Ui.MenuRadioGroup.Value("Mara Lind")[
Ui.MenuRadio.Value("Mara Lind")["Mara Lind"],
Ui.MenuRadio.Value("Theo Brandt")["Theo Brandt"]
],
Ui.MenuSeparator,
Ui.MenuItem.Icon(Ui.IconName.ArrowRightStartOnRectangle)["Logout"]
];
}Inset main
Add Inset(true) to Ui.Main to place your content in a bordered panel with rounded corners. On large screens, content scrolls inside the panel, keeping its border and the space around it visible. On smaller screens, the panel becomes full width and the page scrolls normally.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SidebarInsetExample : Component
{
// The page's body: whatever holds Ui.Main is the layout grid, and the three are siblings in it.
protected override Component? Render() =>
Div.Class("min-h-dvh bg-zinc-50 dark:bg-zinc-900")[
Navigation(),
Bar(),
Ui.Main.Inset(true).Class("lg:ms-0")[
Ui.Heading.Level(1).Xl["Good afternoon, Mara"],
Ui.Text.Class("mt-2 mb-6 text-base")["Here's what's new today"],
Ui.Separator.Subtle
]
];
private static Component Navigation() =>
Ui.Sidebar.Sticky(true).Collapsible(Ui.SidebarCollapsible.Always)
.Class("bg-zinc-50 dark:bg-zinc-900")[
Ui.SidebarHeader[
Ui.SidebarBrand.Href("#").Logo("/img/rask-mark.svg").Name("Acme Inc."),
Ui.SidebarCollapse.Class("sidebar-desktop:-mr-2 sidebar-rail:mr-0")
],
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Home).Href("#").Current(true)["Home"],
Ui.SidebarItem.Icon(Ui.IconName.Inbox).Badge("12").Href("#")["Inbox"],
Ui.SidebarItem.Icon(Ui.IconName.DocumentText).Href("#")["Documents"],
Ui.SidebarItem.Icon(Ui.IconName.Calendar).Href("#")["Calendar"],
Ui.SidebarGroup.Expandable(true).Icon(Ui.IconName.Star).Heading("Favorites")[
Ui.SidebarItem.Href("#")["Marketing site"],
Ui.SidebarItem.Href("#")["Android app"],
Ui.SidebarItem.Href("#")["Brand guidelines"]
]
],
Ui.SidebarSpacer,
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Cog6Tooth).Href("#")["Settings"],
Ui.SidebarItem.Icon(Ui.IconName.InformationCircle).Href("#")["Help"]
],
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start).Class("flex max-lg:hidden")[
Ui.SidebarProfile.Avatar("/img/avatars/amber.svg").Name("Mara Lind"),
Account()
]
];
// A phone's bar: the toggle that slides the sidebar in, and the account.
private static Component Bar() =>
Ui.Header.Class("bg-white lg:hidden dark:bg-zinc-800")[
Ui.SidebarToggle.Icon(Ui.IconName.Bars2).Inset(Ui.Position.Left),
Ui.Spacer,
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start)[
Ui.Profile.Avatar("/img/avatars/amber.svg"),
Account()
]
];
private static Component Account() =>
Ui.Menu[
Ui.MenuRadioGroup.Value("Mara Lind")[
Ui.MenuRadio.Value("Mara Lind")["Mara Lind"],
Ui.MenuRadio.Value("Theo Brandt")["Theo Brandt"]
],
Ui.MenuSeparator,
Ui.MenuItem.Icon(Ui.IconName.ArrowRightStartOnRectangle)["Logout"]
];
}Set the background around the panel on your layout and sidebar. Add lg:ms-0 to the main to remove the extra margin beside the sidebar.
Keep the header, sidebar, and main as direct siblings so the panel can adjust to the header's height. If your layout includes a header, use lg:ms-0 lg:mt-0 to also remove the margin below it.
Secondary header
Use a top header for secondary navigation.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SidebarSecondaryHeaderExample : Component
{
// The page's body: whatever holds Ui.Main is the layout grid, and the three are siblings in it.
protected override Component? Render() =>
Div.Class("min-h-screen bg-white antialiased dark:bg-zinc-800")[
Navigation(),
Bar(),
Ui.Main[
Ui.Heading.Level(1).Xl["Good afternoon, Mara"],
Ui.Text.Class("mt-2 mb-6 text-base")["Here's what's new today"],
Ui.Separator.Subtle
]
];
private static Component Navigation() =>
Ui.Sidebar.Sticky(true).Collapsible(Ui.SidebarCollapsible.Mobile)
.Class("border-r border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900")[
Ui.SidebarHeader[
Ui.SidebarBrand.Href("#").Logo("/img/rask-mark.svg").Name("Acme Inc."),
Ui.SidebarCollapse.Class("lg:hidden")
],
Ui.SidebarSearch.Placeholder("Search..."),
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Home).Href("#").Current(true)["Home"],
Ui.SidebarItem.Icon(Ui.IconName.Inbox).Badge("12").Href("#")["Inbox"],
Ui.SidebarItem.Icon(Ui.IconName.DocumentText).Href("#")["Documents"],
Ui.SidebarItem.Icon(Ui.IconName.Calendar).Href("#")["Calendar"],
Ui.SidebarGroup.Expandable(true).Heading("Favorites")[
Ui.SidebarItem.Href("#")["Marketing site"],
Ui.SidebarItem.Href("#")["Android app"],
Ui.SidebarItem.Href("#")["Brand guidelines"]
]
],
Ui.SidebarSpacer,
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Cog6Tooth).Href("#")["Settings"],
Ui.SidebarItem.Icon(Ui.IconName.InformationCircle).Href("#")["Help"]
],
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start).Class("flex max-lg:hidden")[
Ui.SidebarProfile.Avatar("/img/avatars/amber.svg").Name("Mara Lind"),
Account()
]
];
// Two navbars: a phone's bar, and the section's own links, which stay at every width.
private static Component Bar() =>
Ui.Header.Class("block! border-b border-zinc-200 bg-white lg:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900")[
Ui.Navbar.Class("w-full lg:hidden")[
Ui.SidebarToggle.Icon(Ui.IconName.Bars2).Inset(Ui.Position.Left),
Ui.Spacer,
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start)[
Ui.Profile.Avatar("/img/avatars/amber.svg"),
Account()
]
],
Ui.Navbar.Scrollable()[
Ui.NavbarItem.Href("#").Current(true)["Dashboard"],
Ui.NavbarItem.Badge("32").Href("#")["Orders"],
Ui.NavbarItem.Href("#")["Catalog"],
Ui.NavbarItem.Href("#")["Configuration"]
]
];
private static Component Account() =>
Ui.Menu[
Ui.MenuRadioGroup.Value("Mara Lind")[
Ui.MenuRadio.Value("Mara Lind")["Mara Lind"],
Ui.MenuRadio.Value("Theo Brandt")["Theo Brandt"]
],
Ui.MenuSeparator,
Ui.MenuItem.Icon(Ui.IconName.ArrowRightStartOnRectangle)["Logout"]
];
}Collapsible sidebar
Make your app feel more spacious with a collapsible sidebar.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SidebarCollapsibleExample : Component
{
// The page's body: whatever holds Ui.Main is the layout grid, and the three are siblings in it.
protected override Component? Render() =>
Div.Class("min-h-screen bg-white antialiased dark:bg-zinc-800")[
Navigation(),
Bar(),
Ui.Main[
Ui.Heading.Level(1).Xl["Good afternoon, Mara"],
Ui.Text.Class("mt-2 mb-6 text-base")["Here's what's new today"],
Ui.Separator.Subtle
]
];
private static Component Navigation() =>
Ui.Sidebar.Sticky(true).Collapsible(Ui.SidebarCollapsible.Always)
.Class("border-r border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900")[
Ui.SidebarHeader[
Ui.SidebarBrand.Href("#").Logo("/img/rask-mark.svg").Name("Acme Inc."),
Ui.SidebarCollapse.Class("sidebar-desktop:-mr-2 sidebar-rail:mr-0")
],
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Home).Href("#").Current(true)["Home"],
Ui.SidebarItem.Icon(Ui.IconName.Inbox).Badge("12").Href("#")["Inbox"],
Ui.SidebarItem.Icon(Ui.IconName.DocumentText).Href("#")["Documents"],
Ui.SidebarItem.Icon(Ui.IconName.Calendar).Href("#")["Calendar"],
Ui.SidebarGroup.Expandable(true).Icon(Ui.IconName.Star).Heading("Favorites")[
Ui.SidebarItem.Href("#")["Marketing site"],
Ui.SidebarItem.Href("#")["Android app"],
Ui.SidebarItem.Href("#")["Brand guidelines"]
]
],
Ui.SidebarSpacer,
Ui.SidebarNav[
Ui.SidebarItem.Icon(Ui.IconName.Cog6Tooth).Href("#")["Settings"],
Ui.SidebarItem.Icon(Ui.IconName.InformationCircle).Href("#")["Help"]
],
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start).Class("flex max-lg:hidden")[
Ui.SidebarProfile.Avatar("/img/avatars/amber.svg").Name("Mara Lind"),
Account()
]
];
// A phone's bar: the toggle that slides the sidebar in, and the account.
private static Component Bar() =>
Ui.Header.Class("lg:hidden")[
Ui.SidebarToggle.Icon(Ui.IconName.Bars2).Inset(Ui.Position.Left),
Ui.Spacer,
Ui.Dropdown.Position(Ui.DropdownPosition.Top).Align(Ui.DropdownAlign.Start)[
Ui.Profile.Avatar("/img/avatars/amber.svg"),
Account()
]
];
private static Component Account() =>
Ui.Menu[
Ui.MenuRadioGroup.Value("Mara Lind")[
Ui.MenuRadio.Value("Mara Lind")["Mara Lind"],
Ui.MenuRadio.Value("Theo Brandt")["Theo Brandt"]
],
Ui.MenuSeparator,
Ui.MenuItem.Icon(Ui.IconName.ArrowRightStartOnRectangle)["Logout"]
];
}Behaviour
No script
Flux drives the sidebar with JavaScript; here each state is a checkbox inside the sidebar and each control a <label> for it, so both work on a prerendered page before anything has loaded. Below Breakpoint (Lg, 1024px, unless it says otherwise) the sidebar is off-screen, Ui.SidebarToggle slides it over the page and a click on the backdrop slides it back. From Breakpoint up it is docked, the toggle is not shown, and with Collapsible(Ui.SidebarCollapsible.Always) Ui.SidebarCollapse, or a click anywhere on the rail, narrows it to a 56px rail of icons and widens it again. The labels carry role="button" and a tab stop, and the runtime presses them on Enter and Space.
One sidebar a page: the controls find it by fixed ids (sidebar-open, sidebar-rail), as Flux's find it by a page-wide event. The sidebar's ground and border are your classes, as in Flux's examples.
What the runtime keeps
The two states are the reader's, and neither is a prop, as neither is in Flux. A sidebar slid over the page is put away when the app navigates, or when the row of the page already open is pressed (data-rask-uncheck-on-navigate). The rail is remembered across visits in localStorage under Flux's own key, flux-sidebar-collapsed-desktop (data-rask-persist); Persist(false) is Flux's persist="false".
A Server page restores the rail before its first paint. A WebAssembly app's runtime loads after the prerendered page is on screen, so it adds Ui.SidebarScript to its HeadAssets, beside Ui.AppearanceScript: a few lines that check the box as the parser reaches it, so a narrow sidebar never opens wide and snaps shut. The same script keeps a press on the collapse control made before the hooks have loaded.
Items and groups in the rail
Ui.SidebarItem is a NavLink underneath, so the current page is worked out from the route (aria-current="page") unless Current states it. In the rail it is its icon: the label stays for a screen reader, the badge is dropped, and the item's tooltip, a real Ui.Tooltip to the right of the icon, says the label (or Tooltip). Ui.SidebarCollapse is named by its tooltip at every width, "Toggle sidebar" unless Tooltip says otherwise, and Ui.SidebarSearch shows its placeholder the same way.
Ui.SidebarGroup is a heading over its items, or with Expandable(true) a native <details> that folds with no round trip; Expanded and OnToggle hand that to C#. In the rail a group is its Icon, and its items are a menu beside it: it opens while the pointer is on the icon or the menu, on a press, and from the keyboard (Enter, Space or ArrowDown on the icon, the arrows through the rows, Escape back to the icon). A group without an icon is not shown in the rail.
Search and profile
Ui.SidebarSearch is a button that looks like a field, as Flux draws it: the way into a search dialog or a command palette that a Ui.ModalTrigger around it opens. Given OnInput it is a real field for a sidebar that filters its own list.
Ui.SidebarProfile draws the avatar (the picture, or the initials of Name), and a dropdown around it makes it the trigger of the account menu. Narrowed to the rail it is the avatar alone.
Reference
Ui.Sidebar
The application's navigation, beside the page. It paints no ground of its own.
Step | Takes | What it does |
|---|---|---|
.Sticky() | bool | Keeps the docked sidebar in view while the page scrolls, scrolling inside itself when it is taller. |
.Collapsible(…) | Ui.SidebarCollapsibleNever Mobile Always Each is a step of its own too, unless an HTML tag has its name. | When it can be put away: slid over a phone (Mobile), and narrowed to a rail too (Always). Never when unset. |
.Breakpoint(…) | Ui.BreakpointSm Md Lg Xl Each is a step of its own too, unless an HTML tag has its name. | The width it docks from. Lg, 1024px, when unset. |
.Persist() | bool | Whether the rail is remembered across visits. On unless false. |
.Stashable() | bool | Flux's deprecated spelling of Collapsible(Ui.SidebarCollapsible.Mobile). |
Ui.SidebarHeader
The top of the sidebar, for its brand and its collapse control.
No steps of its own: what goes inside it is its content.
Ui.SidebarBrand
The logo and the name, linking home. Narrowed to the rail it is the logo alone.
Step | Takes | What it does |
|---|---|---|
.Href(…) | a route, or a URL string | Where it leads. A generated route navigates inside the app. |
.Logo(…) | string | The logo image's address. |
.LogoDark(…) | string | The logo shown in dark mode instead: Flux's logo:dark. |
.Name(…) | string | The name beside the logo. |
Ui.SidebarCollapse
Narrows a docked sidebar to its rail, and closes one slid over the page.
Step | Takes | What it does |
|---|---|---|
.Inset(…) | Ui.PositionDefault Top Right Bottom Left Each is a step of its own too, unless an HTML tag has its name. | Pulls the control into its container's padding on that side. |
.Tooltip(…) | string | What the control is called. "Toggle sidebar" when unset. |
Ui.SidebarSearch
A button that looks like a field, the way into a search; a real field once given OnInput.
Step | Takes | What it does |
|---|---|---|
.Placeholder(…) | string | The words shown while nothing has been typed, and its tooltip in the rail. |
.Value(…) | string | What the reader has typed, for a search that is a field. |
.OnInput(…) | a handler given the text: text => … | Runs on every keystroke with the text so far, and makes the search a field. |
Ui.SidebarNav
A list of items and groups: a navigation landmark.
No steps of its own: what goes inside it is its content.
Ui.SidebarItem
One destination. Its children are its label; in the rail it is its icon and a tooltip.
Step | Takes | What it does |
|---|---|---|
.Href(…) | a route, or a URL string | Where it leads. A generated route navigates inside the app; a string is an ordinary link. |
.Icon(…) | Ui.IconName — one of 317 names | The icon before the label, and all that is left of the item in the rail. |
.Badge(…) | string | A short value at the item's trailing edge: a count, a state. |
.Current() | bool | Whether this is the page being shown. Unset, the router decides. |
.Tooltip(…) | string | What the tooltip says while the rail hides the label. The label when unset. |
Ui.SidebarGroup
Items under a heading, which can fold away; in the rail, its icon and a menu.
Step | Takes | What it does |
|---|---|---|
.Heading(…) | string | The words above the group's items. |
.Expandable() | bool | Lets the reader fold the group away and open it again: a native <details>. |
.Icon(…) | Ui.IconName — one of 317 names | The icon before the heading, in place of the chevron, and what the group is in the rail. |
.Expanded() | bool | Whether an expandable group is open. Open unless false. |
.OnToggle(…) | a handler given the state it is now in: open => … | Runs when the reader opens or folds the group, with the state asked for. |
Ui.SidebarSpacer
Takes the leftover height, pushing what follows to the bottom.
No steps of its own: what goes inside it is its content.
Ui.SidebarProfile
Who is signed in, at the foot: the trigger of the account menu a dropdown puts around it.
Step | Takes | What it does |
|---|---|---|
.Avatar(…) | string | The picture's address. Without one the name's initials stand in. |
.Name(…) | string | The person's name. |
Ui.SidebarToggle
The button that slides the sidebar over the page: the hamburger in a phone's header. Not shown from the sidebar's breakpoint up.
Step | Takes | What it does |
|---|---|---|
.Icon(…) | Ui.IconName — one of 317 names | The icon drawn. Bars2 when unset. |
.Inset(…) | Ui.PositionDefault Top Right Bottom Left Each is a step of its own too, unless an HTML tag has its name. | Pulls the button into its container's padding on that side: Flux's inset="left". |
Ui.Main
The page's content, beside the sidebar and under the header. A <div>: put a <main> inside it.
Step | Takes | What it does |
|---|---|---|
.Inset() | bool | A bordered, rounded panel with a gutter round it; from lg up the layout fills the viewport and the panel scrolls. |
.Container() | bool | Holds the content to the container width, centred: inside the panel, when inset. |