Veto
Components

Handbook page

A documentation article and its contents rail, built from one list of sections so the two can never disagree about what is on the page.

On its own

VetoHandbookPage renders an article and nothing else, so it drops into any layout. Sections come from a list rather than markup, which is what lets the contents rail be generated from the same source.

Guides

Getting started

Install the package and render your first component.

# Install

Add the package to any Blazor project. Veto targets .NET 10 and brings no runtime dependencies of its own.

# Register the services

Call AddVeto() in Program.cs. In a Blazor Web App call it in both projects, so components resolve while prerendering as well as after WebAssembly takes over.

# Verify

Drop a component on a page. If it renders with a focus ring when you tab to it, the stylesheet is wired up correctly.

razor
<VetoHandbookPage Eyebrow="Guides"
                  Title="Getting started"
                  Lead="Install the package and render your first component."
                  Sections="Sections" />

@code {
    private static readonly IReadOnlyList<VetoHandbookSection> Sections =
    [
        new() { Id = "install", Title = "Install", Body = "…" },
        new() { Id = "register", Title = "Register the services", Body = "…" }
    ];
}

With a contents rail

VetoHandbookToc takes the same list. Hover a heading to reveal its anchor link; both the rail and the anchors point at a path-qualified URL, which is what makes them work in an app that declares a base href.

Configuration

# Install

Add the package to any Blazor project. Veto targets .NET 10 and brings no runtime dependencies of its own.

# Register the services

Call AddVeto() in Program.cs. In a Blazor Web App call it in both projects, so components resolve while prerendering as well as after WebAssembly takes over.

# Verify

Drop a component on a page. If it renders with a focus ring when you tab to it, the stylesheet is wired up correctly.

razor
<VetoDocsLayout Items="Nav">
    <Toc><VetoHandbookToc Sections="Sections" /></Toc>
    <ChildContent>
        <VetoHandbookPage Title="Configuration" Sections="Sections" />
    </ChildContent>
</VetoDocsLayout>

Landing highlight

Share one VetoHandbookState between the page and the rail and choosing a section highlights where you landed. It fades after five seconds by default, which is long enough to notice and short enough not to linger.

Try it

# Install

Add the package to any Blazor project. Veto targets .NET 10 and brings no runtime dependencies of its own.

# Register the services

Call AddVeto() in Program.cs. In a Blazor Web App call it in both projects, so components resolve while prerendering as well as after WebAssembly takes over.

# Verify

Drop a component on a page. If it renders with a focus ring when you tab to it, the stylesheet is wired up correctly.

razor
<VetoHandbookToc Sections="Sections" State="_state" />

<VetoHandbookPage Title="Try it"
                  Sections="Sections"
                  State="_state"
                  HighlightOnNavigate="true"
                  HighlightDuration="5000"
                  HighlightPersists="false" />

@code {
    private readonly VetoHandbookState _state = new();
}

Following the reader

ScrollSpy marks the section being read, not only the one last clicked, as the reader scrolls. ScrollSpyOffset is how far below the top of the viewport a heading counts as reached - the height of a fixed app bar, plus a little. The rail only hears from the page when the section changes, never per scroll event.

A longer page

# Plan

Scroll through this page and watch the rail keep up. Section 1 of four.

# Build

Scroll through this page and watch the rail keep up. Section 2 of four.

# Test

Scroll through this page and watch the rail keep up. Section 3 of four.

# Ship

Scroll through this page and watch the rail keep up. Section 4 of four.

razor
<VetoHandbookToc Sections="_sections" ScrollSpy="true" ScrollSpyOffset="96" />
<VetoHandbookPage Title="A longer page" Sections="_sections" />

Why the anchors are path-qualified

A bare #section link resolves against the document's base URL, not the address bar. Blazor apps nearly always declare <base href="/">, so such a link sends the reader to the home page instead of down the current one. Both components build the full path alongside the fragment, so the link does what it looks like it does.

Sections also set scroll-margin-top from the layout's app-bar height, so an anchored heading lands below a fixed bar rather than underneath it.

VetoHandbookPage

Parameter Type Default Description
Sections IReadOnlyList<VetoHandbookSection> [] The page's sections, in order.
Title string? null The page heading.
Eyebrow string? null A short label above the title.
Lead string? null One sentence saying what the page covers.
Header RenderFragment? null Extra content in the header, below the lead.
State VetoHandbookState? null Share one instance with the contents rail to enable the landing highlight.
HighlightOnNavigate bool true Highlights the section the reader jumps to.
HighlightDuration int 5000 How long the highlight lasts, in milliseconds.
HighlightPersists bool false Keeps the highlight until another section is chosen.

VetoHandbookToc

Parameter Type Default Description
Sections IReadOnlyList<VetoHandbookSection> [] The same list the page was given.
Title string? "On this page" The heading above the list. Null renders none.
Label string "On this page" The navigation landmark's accessible name.
State VetoHandbookState? null Share one instance with the page to enable the landing highlight.
ScrollSpy bool false Mark the section the reader has scrolled to as current, not only the one last clicked.
ScrollSpyOffset int 96 Pixels below the top of the viewport at which a heading counts as reached.

VetoHandbookSection

Parameter Type Default Description
Id string required The anchor. Used as the element id and the link target, so keep it URL-safe.
Title string required The section heading.
Body string? null The body as plain text. Ignored when Content is set.
Content RenderFragment? null The body as markup - code blocks, lists, components.
Something went wrong. Reload 🗙