You're reading the Rask v1.0.0 guides.View source
All guides

The UI kit (Rask.Ui)

Every daisyUI component, as a typed Rask component. The kit wraps daisyUI 5 — vendored, compiled at the kit's own build, and shipped inside the package — so a Rask app gets the whole component vocabulary without a single utility string, an npm install, or a Tailwind configuration of its own.

It is markup and nothing else: no data access, no host dependency, and no JavaScript. It runs on the ASP.NET host and in browser-WebAssembly, which is the one place it differs from Rask.Dashboard — the console is deliberately server-only because its panels read a DbContext.


dotnet add package Rask.Ui

Live, on this site: Actions · Data display · Navigation · Feedback · Data input · Layout & mockups.

Wiring it up

Two things, and forgetting either produces a page that renders structurally correct components with no colour at all — so both are worth doing before anything else.

1. Link the stylesheet. Opt into the build writing it, then link it:


<PropertyGroup>
  <RaskUiWriteStylesheet>true</RaskUiWriteStylesheet>
</PropertyGroup>

protected override Component? HeadAssets =>
[
    Link.Rel("stylesheet").Href(UiStylesheet.Href(LiveOptions.PathBase)),   // the kit's, FIRST
    Link.Rel("stylesheet").Href(LiveOptions.PathBase + "/css/app.css"),
];

UiStylesheet.Href() carries a content hash, so the file can be cached hard and still change when the kit does. A library that renders kit components into somebody else's host wants no file in a wwwroot it does not own; that case keeps UiStylesheet.Css and inlines it in a <style>, which is what Rask.Dashboard does.

2. Turn the theme on. Nothing in the kit has a colour until an ancestor carries the theme scope:


protected override Component Shell(Component head, Component body) =>
    Html.Lang("en").Attributes((UiStylesheet.ThemeScopeAttribute, ""))[head, body];

The scope exists so that referencing this package cannot repaint an application that only wanted a button. daisyUI paints :root by default; the kit confines it to [data-rask-ui] instead, and the same reasoning is why it ships no preflight.

Order is the contract. The kit's sheet declares the palette, so redefining a token in your own @theme re-skins every component without overriding a single rule — which only works while your copy is what the cascade reads last.

CSS layers do not merge across <link> elements. If you find a kit rule beating one of your utilities, or the reverse, that is why — and it is not something either sheet's source order can settle.

Themes

daisyUI's 35 themes all ship, as the UiThemeName enum. Light is the default and dark follows the operating system; to choose one, put data-theme on the element carrying the theme scope — or on any container, to re-theme just that subtree.


UiThemeController.Label("Dark").Theme(UiThemeName.Dark).Active(_theme is UiThemeName.Dark)
                 .OnChange(theme => _theme = theme)

The control reports a choice and cannot apply it: the palette is set by an ancestor, and no component can write an attribute onto something above it. The page holds the value and writes UiTheme.Value(theme) there — which is also what lets it be persisted, something daisyUI's CSS-only theme-controller could not offer, since nothing in C# knew which theme was showing.

UiThemePicker and UiThemeDropdown are ready-made pickers over the whole set.

The three axes

Colour, fill and size are independent and compose, so an outlined error button needs no member of its own:


UiButton.Label("Delete").Tone(UiTone.Error).Variant(UiVariant.Outline).Size(UiSize.Lg)
Enum Members
UiTone Neutral Primary Secondary Accent Info Success Warning Error
UiVariant Solid Outline Soft Dash Ghost Link
UiSize Default Xs Sm Md Lg Xl

These are daisyUI's own words, deliberately. Translating them into a private vocabulary was the first thing this kit did and the first thing it stopped doing: daisyUI's documentation is the documentation for everything the components render, and a second set of words made every example a translation.

Not every component honours every member — daisyUI defines no input-outline, and no tooltip-neutral — and a member a component has no class for writes nothing, rather than a class that would sit in the markup looking as though it styled something.

Other axes follow the same rule: UiPlacement, UiModalPlacement, UiMaskShape, UiLoadingShape, UiSwapAnimation, UiAuraStyle, UiTabStyle, UiMarker, UiOpenOn.

What is in it

Grouped as daisyUI groups them, so its documentation reads straight across.

Actions UiButton UiDropdown UiModal UiSwap UiThemeController UiFab
Data display UiAccordion UiAccordionSection UiCollapse UiAvatar UiAura UiBadge UiCard UiCarousel UiChatBubble UiCountdown UiDiff UiHover3d UiHoverGallery UiKbd UiList UiListRow UiStat UiStatusDot UiTable UiTextRotate UiTimeline
Navigation UiBreadcrumbs UiDock UiLink UiMegamenu UiMegamenuPanel UiMenu UiMenuItem UiNavbar UiPagination UiSteps UiStep UiTabs UiTab
Feedback UiAlert UiLoading UiProgress UiRadialProgress UiSkeleton UiToast UiTooltip
Data input UiInput UiTextarea UiSelect UiFileInput UiCheckbox UiToggle UiRadio UiRange UiRating UiFieldset UiValidator UiLabel UiFloatingLabel UiOtp UiFilter UiCalendar
Layout UiDivider UiDrawer UiFooter UiHero UiIndicator UiJoin UiStack UiMask
Mockup UiMockupBrowser UiMockupCode UiMockupPhone UiMockupWindow
Chrome UiShell UiTopBar UiBrand UiNav UiNavTab UiCrumbSwitcher UiCrumbSeparator UiTopLink UiMain UiHeader UiGrid UiNotice UiMetricRow UiMetric UiDetailList UiDetailRow UiCode UiSearch
Support UiIcon / UiIconName, UiTheme / UiThemeName, UiStyles, UiStylesheet

Who owns the state

The kit ships no JavaScript, and that constraint decides the shape of every interactive component. It resolves three ways, and which one a component takes is a property of what the platform can do rather than of anyone's preference.

The browser owns it, declaratively. UiModal with an Id and a Trigger is a real <dialog popover>: the browser supplies the top layer, Escape, light-dismiss and a native ::backdrop. UiMegamenu is built the same way. UiFab opens on :focus-within because daisyUI defines no class to force it. All of these work on a prerendered page with no runtime booted, and with scripting off entirely.


UiModal.Title("Shortcuts").Id("shortcuts").Trigger("Show shortcuts")[ … ]

The page owns it, in C#. UiDropdown, UiCollapse, UiAccordion, UiSwap, UiTabs and UiModal's Open path hold their state in a field and redraw through the live diff — which is what lets a dropdown close itself when the action inside it completes.


UiDropdown.Trigger("Actions").Open(_open).OnToggle(open => _open = open)[ … ]

Open is nullable and the three settings mean three things: unset is uncontrolled and the browser decides; true and false hand it to the page. Closed writes dropdown-close rather than merely omitting dropdown-open, because daisyUI also opens on :focus-within — without it, tabbing into the panel would re-open a dropdown the page had just closed.

The markup owns it. UiTab is a real link with a real URL, so a tab is bookmarkable, survives a refresh and answers the back button. UiDrawer keeps its checkbox because daisyUI's rules are written against .drawer-toggle:checked; C# sets it and hears it change, but the input is the component.

And one that lets you choose. UiSelect is the platform's <select> by default and draws its own list when Native is false — a [popover] role="listbox" under a role="combobox" box, with the arrow keys, Home/End, Enter, and a roving aria-activedescendant cursor that skips unavailable options. Reach for it when the list must carry more than the platform will show, or must escape an overflow: hidden ancestor. Both modes take the same properties and mean the same thing by them; what differs is that the drawn list needs the runtime, where the native control works on a prerendered page and with scripting off. That is why the default is native.

The browser still owns dismissal there — Escape and click-outside — and C# hears it through OnToggle, which is what keeps aria-expanded truthful rather than drifting the moment the list is dismissed.

Form controls

All twelve of the kit's data-input controls implement IFormControl<T>, so each works in the two shapes every Rask input does:


Form.Model(_order)[
    UiSelect.Bind(() => _order.Country).Options(countries).Label("Country"),
    UiSelect.Value(_country).Options(countries).Label("Country").OnChange(v => _country = v)
]

The opening step fixes the type argument and the mode together. Bind opens the bound chain and Value the controlled one; they are mutually exclusive because a control with both would have two sources of truth for one field. Label, Options and the rest follow in any order, since none of them says anything about T. Bound mode drives the surrounding Form's validation — per-field Validate, AfterBind, and the aria-invalid/aria-describedby display — and controlled mode leaves the value with the parent. See building form controls.

Generic where the value type varies, concrete where it does not. UiInput<T>, UiTextarea<T>, UiSelect<T> and UiFilter<T> are generic — the model decides what they hold, and UiInput even takes its type attribute from T, so a bound int is a number field with nothing said at the call site. The rest are closed over the one type they can have: UiCheckbox, UiToggle and UiRadio over bool, UiRange over double, UiRating over int, UiOtp and UiFileInput over string, UiCalendar over DateOnly. A checkbox's value is a bool and nothing else; a type parameter there would have exactly one legal argument.

Binds
UiInput<T> UiTextarea<T> UiSelect<T> what the field holds
UiFilter<T> the chosen option of a whole radio group
UiRadio whether this option is the chosen one — the group's value belongs to UiFilter<T>
UiCheckbox UiToggle on or off
UiRange UiRating UiCalendar the position, the star count, the day
UiOtp the code — OnComplete fires on the transition into a full one, in both modes
UiFileInput the chosen file's name, write-only — a browser refuses to have a file input's value set, so binding fills the model and never the box. The bytes come through OnFiles.

A field with no value yet opens on its type alone: UiInput.Of<string>().Label("Search"). A form control's openings are its mode pins, so a required step like Label never gets to pin T — without Of a controlled field with nothing in it would have to invent a value to compile. Of is the controlled mode: the parent still owns whatever the field ends up with.

UiCalendar is the one to read twice. Month and OnMonth are the view, not the value — paging through months changes nothing a form would submit, which is why they sit outside the binding.

The rule the whole kit rests on

daisyUI emits a component's CSS only where Tailwind can see its class name in the scanned source. A name built by concatenation — "btn-" + tone — is a name no scanner ever reads, so the class is absent from the compiled sheet and the component renders with no styling whatsoever. Not misaligned, not the wrong colour: unstyled. The build stays green and the markup carries exactly the class the call site asked for.

That is why every class the kit can write is spelled out as a complete literal in UiClassNames.cs, why the axes above are closed enums rather than strings, and why UiTextRotate has no Interval property — daisyUI reads its speed from a duration-* utility, and turning a TimeSpan into a class name at run time is exactly the failure this rule prevents.

If you write your own ui-* classes, the same applies to you: copy the kit's @theme block into your own stylesheet, because Tailwind emits a utility only where it can see the token.

Two rules it holds itself to

Mobile-first, which is a different claim from responsive. Every control takes a 44px touch target below sm. A dialog is a bottom sheet on a phone and a centred card above it, because a centred dialog at 360px either overflows or shrinks its content past reading. The tab bar scrolls sideways rather than wrapping, so the header is exactly one row tall however many tabs there are.

Every control has a name. A label is required, not optional, and it becomes the accessible name rather than a placeholder — a placeholder disappears the moment typing starts, so the one thing saying what a field is for vanishes exactly when a reader might check it. An icon-only button puts its label in aria-label; a spinner is aria-hidden with its words beside it; a failed toast changes its icon and not only its colour.

Names

Every component carries the Ui prefix, and that is load-bearing rather than decorative. Inside a markup host a bare X is the chain's Build<X> entry, so a component called Shell would collide with Component.Shell, and Nav/Main/Button/Select/Search with the Rask.Html tags. On a collision RASK040 gives neither type an entry, across the whole compilation.

The same rule applies to the namespace. Rask.Ui is an enclosing namespace of every Rask.* compilation, so a type of your own named Ui inside a Rask.-rooted namespace will be shadowed by it — C# resolves a simple name against enclosing namespaces before it looks at imports.

Each component lives in a file named after it, so the file list in src/Rask.Ui is the component list.

See also

  • Dashboard — the operator console this kit was extracted from
  • Tailwind — how the compiler is wired into a Rask build
  • Building components — the chain the kit is composed with