Veto
Getting started

Usage

What every Veto component has in common, so the second one you learn costs less than the first.

The shared contract

Every component - input, layout, overlay - takes the same six parameters. Learn them once.

On every component

Parameter Type Default Description
Class string? null Extra CSS classes, appended after the component's own so yours always win.
Style string? null Inline styles, appended after the component's own.
Id string? null The root element's id. Generated when you don't supply one, so labels and aria references always resolve.
TestId string? null Rendered as data-testid. The stable selector for end-to-end tests.
Visible bool true When false the component renders nothing at all.
AdditionalAttributes IReadOnlyDictionary<string, object>? null Any attribute you pass that isn't a parameter, splatted onto the root element.

The box parameters

Every component also carries a set of box-level parameters: its size, the space around and inside it, how it treats overflow, where it sits and how it stacks. They exist so the one place a component has to be a particular width, or sit above something it overlaps, needs no wrapper element whose only job is to hold a style attribute.

They are emitted as inline declarations before your own Style, so an explicit style still wins. Two of them carry a consequence worth knowing: RootZIndex and the offsets also make the root position: relative, because on a statically positioned element neither does anything at all - and a component that positions itself, like a fixed toast stack or a sticky footer, is left alone.

They carry a Root prefix because they have to. Blazor matches parameters by name across a component's whole inheritance chain, case insensitively, so a component declaring its own Width would collide with an inherited one rather than override it - and throw on its first render. The prefix also says which element is meant, which was the ambiguity anyway: VetoDrawer.Width is the width the drawer opens to, and VetoCard.Padding pads the regions inside the card, not its root.

On every component

Parameter Type Default Description
RootWidth / RootHeight string? null CSS lengths for the root element.
RootMinWidth / RootMaxWidth string? null CSS length bounds on the root's width.
RootMinHeight / RootMaxHeight string? null CSS length bounds on the root's height.
RootMargin VetoSpacing? null Space outside the root, as a step on the shared spacing scale.
RootPadding VetoSpacing? null Space inside the root. Distinct from the Padding VetoCard and VetoContainer own.
RootOverflow VetoOverflow? null Visible, Hidden, Auto or Scroll. Visible is how a clipping component is made to let a popover out.
RootTop / RootRight / RootBottom / RootLeft string? null CSS offsets. Setting any of them also positions the root.
RootZIndex int? null The root's stacking order. Also positions the root, since z-index does nothing on a static element.
RootTooltip string? null The root's title attribute. Hover only, so never the only place something is said.

Sizing and spacing without a wrapper

A card given a width, a max-width and a margin, and a button given a min-width so a row of them lines up. None of it needs a div around the component.

Sized

Eighteen rem wide, with a margin of its own.
razor
<VetoCard Title="Sized" RootWidth="18rem" RootMargin="VetoSpacing.Medium">
    Eighteen rem wide, with a margin of its own.
</VetoCard>

<VetoButton RootMinWidth="12rem">Wide enough</VetoButton>

Overflow and stacking

A card clips its contents by default, so a menu inside one is cut off at its edge. Overflow lets the panel out, and ZIndex lifts a component above something it overlaps - both without touching the component's stylesheet.

Clips

Lets it out

razor
@* A card clips by default, so the menu's panel is cut off at its edge. *@
<VetoCard Title="Clips" RootWidth="14rem">
    <VetoMenu Label="Cut off" Icon="@VetoIcons.MoreVertical">…</VetoMenu>
</VetoCard>

@* Overflow lets the panel out; ZIndex lifts it above what it overlaps. *@
<VetoCard Title="Lets it out" RootWidth="14rem" RootOverflow="VetoOverflow.Visible">
    <VetoMenu Label="Escapes" Icon="@VetoIcons.MoreVertical" RootZIndex="20">…</VetoMenu>
</VetoCard>

A native tooltip on anything

Tooltip renders as the root element's title attribute. It appears on hover only - no keyboard, no touch - so it is a convenience for a hint, never the only place something important is said.

razor
<VetoButton RootTooltip="Saves without leaving the page">Save</VetoButton>

Your classes never get overwritten

Class is appended after the component's own classes, and unmatched attributes are splatted onto the element. A component never discards what you passed it.

razor
<VetoTextBox TValue="string"
             Label="Email"
             Class="my-field"
             Style="max-width: 20rem"
             autocomplete="email"
             maxlength="120"
             @bind-Value="_email" />

<!-- renders: class="veto-textbox veto-textbox--md … my-field" -->

Forms work the way Blazor's do

Inputs derive from Blazor's own InputBase<T>. That is not a wrapper - it means @bind-Value, EditForm, DataAnnotationsValidator and ValidationMessage behave exactly as they already do, because it is the same machinery.

razor
<EditForm Model="_model" OnValidSubmit="Save">
    <DataAnnotationsValidator />

    <VetoTextBox Label="Username"
                 Required="true"
                 HelperText="Between 3 and 20 characters."
                 @bind-Value="_model.Username" />

    <button type="submit">Save</button>
</EditForm>

Styling is tokens, not overrides

Components read --veto-* custom properties and never hard-code a colour. To rebrand, override the tokens - don't restyle the components.

csharp
<VetoThemeProvider Theme="AppTheme" />

@code {
    private static readonly VetoTheme AppTheme = new()
    {
        Light = new() { Primary = "#C2121F", Surface = "#FFFFFF" },
        Dark  = new() { Primary = "#FF3B47", Surface = "#0B0A0B" },
        RadiusMedium = "6px"
    };
}

This site is doing exactly that. Its red palette is a VetoTheme, not a single line of component CSS. Flip the theme and watch every component follow:

Testing against Veto

Every component renders data-testid when you give it a TestId, which gives your end-to-end tests a selector that survives restyling and refactoring.

csharp
// Razor
<VetoTextBox TestId="username" Label="Username" @bind-Value="_name" />

// Playwright
await Page.GetByTestId("username").Locator("input").FillAsync("veto-user");
Something went wrong. Reload 🗙