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
<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
@* 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.
<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.
<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.
<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.
<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.
// Razor
<VetoTextBox TestId="username" Label="Username" @bind-Value="_name" />
// Playwright
await Page.GetByTestId("username").Locator("input").FillAsync("veto-user");