Navigation components
TableOfContentsNavigation and OutlineNavigation are the two Razor components in Pennington.UI that render, respectively, the sidebar page tree and the floating in-page heading outline. Both live in namespace Pennington.UI.Components.Navigation and are consumed by Pennington.DocSite's MainLayout, but are available to any host referencing Pennington.UI. TableOfContentsNavigation binds to an ImmutableList<NavigationTreeItem> produced by NavigationBuilder; OutlineNavigation binds to a client-side DOM selector at runtime. Neither accepts a NavigationInfo directly.
TableOfContentsNavigation
Declaration
@if (TableOfContents != null)
{
<nav>
<ul class="flex flex-col @ListGapClass">
@foreach (var tableOfContentEntry in TableOfContents.OrderBy(i => i.Order))
{
@TocEntry(tableOfContentEntry)
}
</ul>
</nav>
}
@code {
/// <summary>Navigation tree to render; when null the component renders nothing, and entries are sorted by <see cref="NavigationTreeItem.Order"/> at each level.</summary>
[Parameter] public ImmutableList<NavigationTreeItem>? TableOfContents { get; set; }
/// <summary>Optional label forwarded from the caller's <c>NavigationInfo.SectionName</c>; not rendered by the default template.</summary>
[Parameter] public string? SectionLabel { get; set; }
/// <summary>CSS classes applied to the outer <c><ul></c> that holds the top-level navigation entries.</summary>
[Parameter] public string ListGapClass { get; set; } = "gap-4";
/// <summary>CSS classes applied to the nested <c><ul></c> that holds a section's child entries.</summary>
[Parameter] public string ChildListClass { get; set; } = "mt-4";
/// <summary>Layout and typography classes applied to the section-header element.</summary>
[Parameter] public string SectionHeaderStructureClass { get; set; } = "font-display font-medium first:pt-0";
/// <summary>CSS classes applied to section-header text — both the plain <c><div></c> for empty-route entries and the <c><a></c> when a top-level entry has children.</summary>
[Parameter] public string SectionHeaderColorClass { get; set; } = "text-base-900 dark:text-base-50";
/// <summary>Layout and typography classes applied to each child-level <c><a></c> element under a section.</summary>
[Parameter] public string LinkStructureClass { get; set; } = "block text-sm w-full border-l pl-3.5 py-1.5";
/// <summary>CSS classes applied to each child-level <c><a></c> for color and <c>data-current=true</c> state, composed after <see cref="LinkStructureClass"/>.</summary>
[Parameter] public string LinkColorClass { get; set; } = "transition-colors transition-300 border-base-300 dark:border-base-800 data-[current=true]:border-primary-400 text-base-500 dark:text-base-400 data-[current=true]:text-primary-800 dark:data-[current=true]:text-primary-500 hover:text-accent-400 dark:hover:text-base-50";
/// <summary>Layout classes applied to a leaf root-level <c><a></c> when a top-level entry has no children.</summary>
[Parameter] public string RootLinkStructureClass { get; set; } = "block w-full py-1";
/// <summary>CSS classes applied to a leaf root-level <c><a></c> (a top-level entry with no children), composed after <see cref="RootLinkStructureClass"/>.</summary>
[Parameter] public string RootLinkColorClass { get; set; } = "transition-colors transition-300 text-base-700 dark:text-base-400 data-[current=true]:text-primary-800 dark:data-[current=true]:text-primary-500 hover:text-accent-400 dark:hover:text-base-50";
private RenderFragment TocEntry(NavigationTreeItem tocEntry) =>
@<li class="block">
@if (tocEntry.Route.CanonicalPath.Value == "")
{
<div class="@SectionHeaderStructureClass @SectionHeaderColorClass">@tocEntry.Title</div>
}
else
{
<a data-current="@tocEntry.IsSelected.ToString().ToLowerInvariant()" href="@tocEntry.Route.CanonicalPath.Value" class="@(tocEntry.Children.Count == 0 ? RootLinkStructureClass + " " + RootLinkColorClass : SectionHeaderStructureClass + " " + SectionHeaderColorClass)">@tocEntry.Title</a>
}
@if (tocEntry.Children.Count > 0)
{
<ul class="@ChildListClass">
@foreach (var childEntry in tocEntry.Children.OrderBy(i => i.Order).Where(i => i.Route.CanonicalPath.Value != ""))
{
<li class="block">
<a data-current="@childEntry.IsSelected.ToString().ToLowerInvariant()" href="@childEntry.Route.CanonicalPath.Value" class="@LinkStructureClass @LinkColorClass">@childEntry.Title</a>
</li>
}
</ul>
}
</li>;
}
Renders an ordered <nav><ul> of NavigationTreeItem entries, recursing one level into each entry's Children collection. Root entries with an empty Route.CanonicalPath render as plain section headers; entries with a path render as anchor links carrying data-current="true" when IsSelected is set.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
TableOfContents |
ImmutableList<NavigationTreeItem>? |
null |
Navigation tree to render; when null the component renders nothing, and entries are sorted by NavigationTreeItem.Order at each level. |
SectionLabel |
string? |
null |
Optional label forwarded from the caller's NavigationInfo.SectionName; not rendered by the default template. |
ListGapClass |
string |
"gap-4" |
CSS classes applied to the outer <ul> that holds the top-level navigation entries. |
ChildListClass |
string |
"mt-4" |
CSS classes applied to the nested <ul> that holds a section's child entries. |
SectionHeaderStructureClass |
string |
"font-display font-medium first:pt-0" |
Layout and typography classes applied to the section-header element. |
SectionHeaderColorClass |
string |
"text-base-900 dark:text-base-50" |
CSS classes applied to section-header text — both the plain <div> for empty-route entries and the <a> when a top-level entry has children. |
LinkStructureClass |
string |
"block text-sm w-full border-l pl-3.5 py-1.5" |
Layout and typography classes applied to each child-level <a> element under a section. |
LinkColorClass |
string |
see source | CSS classes applied to each child-level <a> for color and data-current=true state, composed after LinkStructureClass. |
RootLinkStructureClass |
string |
"block w-full py-1" |
Layout classes applied to a leaf root-level <a> when a top-level entry has no children. |
RootLinkColorClass |
string |
see source | CSS classes applied to a leaf root-level <a> (a top-level entry with no children), composed after RootLinkStructureClass. |
Slots
This component has no RenderFragment slots; all customization is performed through the class-name parameters above.
Example
The DocSite MainLayout (src/Pennington.DocSite/Components/Layout/MainLayout.razor) instantiates TableOfContentsNavigation twice — once per area when DocSiteOptions.Areas is populated and once against the root tree otherwise — passing the tree produced by NavigationBuilder.BuildTree.
OutlineNavigation
Declaration
<div data-role="page-outline" data-content-selector="@ContentSelector" class="relative @ContainerStructureClass @ContainerColorClass">
<div data-role="page-outline-highlighter" class="absolute transition-all duration-500 opacity-0 left-[-1px] w-[1px] bg-primary-400"></div>
<div>
<ul class="@ListStructureClass @ListColorClass"
data-outline-link-structure-class="@OutlineLinkStructureClass"
data-outline-link-color-class="@OutlineLinkColorClass">
@* Outline links will be dynamically generated by JavaScript *@
</ul>
</div>
</div>
@code {
/// <summary>CSS selector the client-side outline script queries to discover heading elements; must be non-empty for the outline to populate.</summary>
[Parameter, EditorRequired] public string ContentSelector { get; set; } = "";
/// <summary>Textual label accepted for parity with other outline skins; not rendered by the default template.</summary>
[Parameter] public string Title { get; set; } = "On This Page";
/// <summary>Layout and border classes applied to the outer <c>data-role="page-outline"</c> container.</summary>
[Parameter] public string ContainerStructureClass { get; set; } = "border-l border-base-200 dark:border-base-800";
/// <summary>CSS classes applied to the outer container for color treatment, composed after <see cref="ContainerStructureClass"/>.</summary>
[Parameter] public string ContainerColorClass { get; set; } = "";
/// <summary>Layout classes applied to the outline <c><ul></c>.</summary>
[Parameter] public string ListStructureClass { get; set; } = "list-none pl-4" ;
/// <summary>CSS classes applied to the <c><ul></c> that holds outline links, composed after <see cref="ListStructureClass"/>.</summary>
[Parameter] public string ListColorClass { get; set; } = "text-neutral-500 dark:text-neutral-400";
/// <summary>CSS classes emitted on the container as <c>data-outline-link-color-class</c> and applied by the client-side script to each generated <c><li><a></c> for color and <c>data-selected=true</c> state.</summary>
[Parameter] public string OutlineLinkColorClass { get; set; } = "transition-colors duration-250 hover:text-base-900 dark:hover:text-base-50/90 data-[selected=true]:text-base-800 dark:data-[selected=true]:text-base-50";
/// <summary>Layout classes emitted on the container as <c>data-outline-link-structure-class</c> and applied by the client-side script to each generated <c><li><a></c>.</summary>
[Parameter] public string OutlineLinkStructureClass { get; set; } = "py-1 ml-[calc(-1*(4em-1px))] pl-[calc(4em+1px)] ";
}
Emits a data-role="page-outline" container and an empty <ul> whose items are populated client-side by scraping headings from the element matched by ContentSelector. The component performs no server-side heading extraction; the companion script in Pennington.UI/wwwroot/ reads data-content-selector, data-outline-link-structure-class, and data-outline-link-color-class to build and highlight the outline in the browser.
Parameters
ContentSelector is [EditorRequired]; all other parameters carry defaults tuned for the DocSite main-content column.
| Name | Type | Default | Description |
|---|---|---|---|
ContentSelector |
string |
"" (required) |
CSS selector the client-side outline script queries to discover heading elements; must be non-empty for the outline to populate. |
Title |
string |
"On This Page" |
Textual label accepted for parity with other outline skins; not rendered by the default template. |
ContainerStructureClass |
string |
"border-l border-base-200 dark:border-base-800" |
Layout and border classes applied to the outer data-role="page-outline" container. |
ContainerColorClass |
string |
"" |
CSS classes applied to the outer container for color treatment, composed after ContainerStructureClass. |
ListStructureClass |
string |
"list-none pl-4" |
Layout classes applied to the outline <ul>. |
ListColorClass |
string |
"text-neutral-500 dark:text-neutral-400" |
CSS classes applied to the <ul> that holds outline links, composed after ListStructureClass. |
OutlineLinkColorClass |
string |
see source | CSS classes emitted on the container as data-outline-link-color-class and applied by the client-side script to each generated <li><a> for color and data-selected=true state. |
OutlineLinkStructureClass |
string |
see source | Layout classes emitted on the container as data-outline-link-structure-class and applied by the client-side script to each generated <li><a>. |
Slots
This component has no RenderFragment slots; the outline list is populated at runtime by the companion client script.
Example
The DocSite MainLayout drops a single <OutlineNavigation ContentSelector="article main" /> into the right-hand rail so the script binds to headings inside the rendered article.
Binding to NavigationInfo
NavigationInfo is the per-request record exposed by NavigationBuilder.BuildNavigationInfo; it carries SectionName, SectionRoute, Breadcrumbs, PageTitle, PreviousPage, and NextPage, not a navigation tree.
/// <summary>Page-scoped navigation context exposed to layouts and components.</summary>
/// <param name="SectionName">Label of the containing top-level section, or null if none.</param>
/// <param name="SectionRoute">Route of the containing top-level section, or null if none.</param>
/// <param name="Breadcrumbs">Breadcrumb trail from the site root to the current page.</param>
/// <param name="PageTitle">Title of the current page.</param>
/// <param name="PreviousPage">Previous page in reading order, or null at the start.</param>
/// <param name="NextPage">Next page in reading order, or null at the end.</param>
public record NavigationInfo(
string? SectionName,
ContentRoute? SectionRoute,
ImmutableList<BreadcrumbItem> Breadcrumbs,
string PageTitle,
NavigationTreeItem? PreviousPage,
NavigationTreeItem? NextPage
);
TableOfContentsNavigation.TableOfContents is populated from the tree returned by NavigationBuilder.BuildTree(items, currentRoute, locale), not from a NavigationInfo; NavigationInfo.SectionName is the value callers typically pass to SectionLabel. OutlineNavigation does not read NavigationInfo at all — it is a client-side component bound to a DOM selector — so previous/next navigation and breadcrumbs flow through other components or layout slots.
See also
- How-to: Customize the sidebar
- Related reference: Navigation types
- Related reference: Content components