Components
Dropdown
A button and the menu it opens. The first child of Ui.Dropdown is the trigger, any button; the second is what it opens: a Ui.Menu of actions, a Ui.Navmenu of links, or a Ui.Popover. The browser opens and closes it, and the page hears only a pick.
Drawn and behaving as Flux UI's dropdown does, measured against its live docs.
Dropdown
A menu with a submenu of radios, one of checkboxes and a danger row. It opens with focus on the menu: the arrows walk the rows and stop at the ends, a letter jumps, Right or Enter opens a submenu and Left closes it, Enter or Space picks, Escape and Tab leave.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownExample : Component
{
private string _sort = "name";
private bool _draft = true;
private bool _published = true;
private bool _archived;
private string _last = "nothing yet";
protected override Component? Render() =>
Div.Class("flex flex-wrap items-center gap-4")[
Ui.Dropdown[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Options"],
Ui.Menu[
Ui.MenuItem.Icon(Ui.IconName.Plus).OnClick(() => { _last = "new post"; })["New post"],
Ui.MenuSeparator.Key("s1"),
Ui.MenuSubmenu.Heading("Sort by")[
Ui.MenuRadioGroup.Value(_sort).OnChange(sort => { _sort = sort; _last = "sorted by " + sort; })[
Ui.MenuRadio.Value("name")["Name"],
Ui.MenuRadio.Value("date")["Date"],
Ui.MenuRadio.Value("popularity")["Popularity"]
]
],
Ui.MenuSubmenu.Heading("Filter")[
Ui.MenuCheckbox.Value(_draft).OnChange(on => { _draft = on; })["Draft"],
Ui.MenuCheckbox.Value(_published).OnChange(on => { _published = on; })["Published"],
Ui.MenuCheckbox.Value(_archived).OnChange(on => { _archived = on; })["Archived"]
],
Ui.MenuSeparator.Key("s2"),
Ui.MenuItem.Danger.Icon(Ui.IconName.Trash).OnClick(() => { _last = "deleted"; })["Delete"]
]
],
Ui.Text[$"Last action: {_last}."]
];
}Last action: nothing yet.
Navigation menus
A Ui.Navmenu is links, not a menu: no menu roles and no cursor, so focus stays on the button and Tab walks the links.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownNavigationExample : Component
{
protected override Component? Render() =>
Ui.Dropdown.Bottom.End[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Rosa Vidal"],
Ui.Navmenu[
Ui.NavmenuItem.Href("#account").Icon(Ui.IconName.User)["Account"],
Ui.NavmenuItem.Href("#profile").Icon(Ui.IconName.BuildingStorefront)["Profile"],
Ui.NavmenuItem.Href("#billing").Icon(Ui.IconName.CreditCard)["Billing"],
Ui.NavmenuItem.Href("#logout").Icon(Ui.IconName.ArrowRightStartOnRectangle)["Logout"],
Ui.NavmenuItem.Href("#delete").Icon(Ui.IconName.Trash).Danger["Delete"]
]
];
}Positioning
A side and an alignment, each a step. With no room there it takes the other side, and it always stays inside the viewport.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownPositionExample : Component
{
protected override Component? Render() =>
Ui.Dropdown.Top.Start[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Above"],
Ui.Navmenu[
Ui.NavmenuItem.Href("#account")["Account"],
Ui.NavmenuItem.Href("#profile")["Profile"],
Ui.NavmenuItem.Href("#billing")["Billing"],
Ui.NavmenuItem.Href("#logout")["Logout"]
]
];
}Offset & gap
Gap is the distance from the button; Offset shifts the menu along it, back past its edge when negative.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownOffsetExample : Component
{
protected override Component? Render() =>
Ui.Dropdown.Offset(-15).Gap(2)[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Offset and gap"],
Ui.Navmenu[
Ui.NavmenuItem.Href("#account")["Account"],
Ui.NavmenuItem.Href("#profile")["Profile"],
Ui.NavmenuItem.Href("#billing")["Billing"],
Ui.NavmenuItem.Href("#logout")["Logout"]
]
];
}Keyboard hints
Kbd shows a shortcut at the end of a row and Suffix a word; the page binds the key itself. A disabled row is shown and stepped over.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownShortcutsExample : Component
{
private string _last = "nothing yet";
protected override Component? Render() =>
Div.Class("flex flex-wrap items-center gap-4")[
Ui.Dropdown[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Shortcuts"],
Ui.Menu[
Ui.MenuItem.Icon(Ui.IconName.PencilSquare).Kbd("⌘S").OnClick(() => { _last = "save"; })["Save"],
Ui.MenuItem.Icon(Ui.IconName.DocumentDuplicate).Kbd("⌘D").OnClick(() => { _last = "duplicate"; })["Duplicate"],
Ui.MenuItem.Icon(Ui.IconName.ArrowDownTray).Suffix("PDF").Disabled()["Export"],
Ui.MenuItem.Icon(Ui.IconName.Trash).Danger.Kbd("⌘⌫").OnClick(() => { _last = "delete"; })["Delete"]
]
],
Ui.Text[$"Last action: {_last}."]
];
}Last action: nothing yet.
Checkbox items
Rows that are on or off: Value with OnChange, or Bind to a model's flag. A pick closes the menu, as in Flux.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownCheckboxExample : Component
{
private bool _read = true;
private bool _write = true;
private bool _delete;
protected override Component? Render() =>
Div.Class("flex flex-wrap items-center gap-4")[
Ui.Dropdown[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Permissions"],
Ui.Menu[
Ui.MenuCheckbox.Value(_read).OnChange(on => { _read = on; })["Read"],
Ui.MenuCheckbox.Value(_write).OnChange(on => { _write = on; })["Write"],
Ui.MenuCheckbox.Value(_delete).OnChange(on => { _delete = on; })["Delete"]
]
],
Ui.Text[_delete ? "May delete." : "May not delete."]
];
}May not delete.
Radio items
One of several, in a Ui.MenuRadioGroup whose type is the type of its values.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownRadioExample : Component
{
private string _by = "latest";
protected override Component? Render() =>
Div.Class("flex flex-wrap items-center gap-4")[
Ui.Dropdown[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Sort by"],
Ui.Menu[
Ui.MenuRadioGroup.Value(_by).OnChange(by => { _by = by; })[
Ui.MenuRadio.Value("latest")["Latest activity"],
Ui.MenuRadio.Value("created")["Date created"],
Ui.MenuRadio.Value("popular")["Most popular"]
]
]
],
Ui.Text[$"Ordered by {_by}."]
];
}Ordered by latest.
Groups
Separators set related rows apart.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownGroupsExample : Component
{
protected override Component? Render() =>
Ui.Dropdown[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Groups"],
Ui.Menu[
Ui.MenuItem["View"],
Ui.MenuItem["Transfer"],
Ui.MenuSeparator.Key("s1"),
Ui.MenuItem["Publish"],
Ui.MenuItem["Share"],
Ui.MenuSeparator.Key("s2"),
Ui.MenuItem.Danger["Delete"]
]
];
}Groups with headings
A Ui.MenuGroup with a Heading names the rows under it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownHeadingsExample : Component
{
protected override Component? Render() =>
Ui.Dropdown[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Headings"],
Ui.Menu[
Ui.MenuGroup.Heading("Account")[
Ui.MenuItem["Profile"],
Ui.MenuItem["Permissions"]
],
Ui.MenuGroup.Heading("Billing")[
Ui.MenuItem["Transactions"],
Ui.MenuItem["Payouts"],
Ui.MenuItem["Refunds"]
],
Ui.MenuItem["Logout"]
]
];
}Keep open
KeepOpen on the menu keeps it up after every pick, for one someone ticks several of. On the rows alone, the other rows still close it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownKeepOpenExample : Component
{
private bool _draft = true;
private bool _published = true;
private bool _archived;
private string _last = "nothing yet";
protected override Component? Render() =>
Div.Class("flex flex-wrap items-center gap-4")[
// On the menu: every row keeps it open.
Ui.Dropdown.Key("menu")[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Keep open"],
Ui.Menu.KeepOpen()[
Ui.MenuCheckbox.Value(_draft).OnChange(on => { _draft = on; })["Draft"],
Ui.MenuCheckbox.Value(_published).OnChange(on => { _published = on; })["Published"],
Ui.MenuCheckbox.Value(_archived).OnChange(on => { _archived = on; })["Archived"]
]
],
// On the rows: they keep it open, and Clear still closes it.
Ui.Dropdown.Key("rows")[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Filters"],
Ui.Menu[
Ui.MenuCheckbox.Value(_draft).KeepOpen().OnChange(on => { _draft = on; })["Draft"],
Ui.MenuCheckbox.Value(_published).KeepOpen().OnChange(on => { _published = on; })["Published"],
Ui.MenuCheckbox.Value(_archived).KeepOpen().OnChange(on => { _archived = on; })["Archived"],
Ui.MenuSeparator,
Ui.MenuItem.Danger.OnClick(() =>
{
_draft = _published = _archived = false;
_last = "cleared the filters";
})["Clear"]
]
],
Ui.Text[$"Last action: {_last}."]
];
}Last action: nothing yet.
Open state
Open hands the state to the page, and OnToggle tells it when the reader opens or closes it. Unset, the browser keeps it and nothing is sent.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class DropdownOpenExample : Component
{
private bool _open;
private string _last = "nothing yet";
protected override Component? Render() =>
Div.Class("flex flex-wrap items-center gap-4")[
Ui.Dropdown.Open(_open).OnToggle(open => { _open = open; })[
Ui.Button.IconTrailing(Ui.IconName.ChevronDown)[_open ? "Close menu" : "Open menu"],
Ui.Menu[
Ui.MenuItem.Kbd("⌘R").OnClick(() => Did("rename"))["Rename"],
Ui.MenuItem.OnClick(() => Did("archive"))["Archive"]
]
],
Ui.Text[$"Last action: {_last}."]
];
private void Did(string action)
{
_last = action;
_open = false;
}
}Last action: nothing yet.
Behaviour
Who owns what
The browser owns the open state. The menu is a [popover] the trigger opens with popovertarget, so the browser opens and closes it (the top layer, Escape, a click outside) with no runtime at all, and it is placed with CSS anchor positioning.
Where the reader is in an open menu is not the page's state either: the runtime keeps the cursor in the browser (data-rask-menu-cursor on the menu), so a pointer gliding down the rows and an arrow key held down send the server nothing (no event, no render, no patch) on either host.
What the page hears is a row being pressed (its OnClick, a checkbox's or a radio's value) and, only when it gave OnToggle, the menu opening and closing. Without OnToggle, opening and closing a dropdown, a popover or a context menu sends nothing, and neither does a Ui.MenuItem with no OnClick: data-open and aria-expanded are written in the browser.
Placement
Every panel the kit opens against a trigger stays inside the viewport as Flux's does: a menu, a popover, a tooltip, a list of options, a calendar, a list of times. When there is no room where it asked to be it takes the other alignment, then the other side, and is slid back to stay 5px inside the edge; one taller than what is left is held to that height and scrolls inside. It is the runtime's data-rask-place hook, with nothing to write in an app.
While a menu is open the page behind does not scroll and does not take the pointer, so a click outside only closes it (data-rask-lock on the menu, a dropdown's and a context menu's alike, as on Flux), and the runtime hands focus back to the trigger after it. A menu the pointer opened, Hover() (data-rask-hover), locks nothing and takes no focus.
Keyboard
Flux's, key for key. The menu opens with focus on it and no row chosen. The row under the cursor has focus (roving focus, not aria-activedescendant), tabindex="0" and data-active, all three written by the runtime and held against the next render, never rendered.
Key | What happens |
|---|---|
ArrowDown on the closed trigger | Opens the menu onto its first row. |
ArrowDown / ArrowUp | The first press puts the cursor on the first row; after that the cursor moves a row at a time, stops at the ends and steps over disabled rows. |
| A letter | Jumps to the row that starts with it. |
ArrowRight or Enter on a submenu's row | Opens the submenu onto its first row. |
ArrowLeft in a submenu | Closes it, with the cursor back on its row. |
Enter or Space | Picks: the runtime presses the focused row, then closes the menu. |
Tab | Leaves the menu, which closes it. |
Escape | Closes it: the browser's own. |
Home, End, PageUp, PageDown | Nothing: they are not menu keys. |
Pointer, and submenus
There is one highlight. A row is lit by data-active and by nothing else, no :hover, whether the pointer or the keyboard lit it. The runtime writes it, in the browser, the moment the pointer enters a row (data-rask-menu-pointer). Focus does not move, as on Flux, so the row the keyboard was on keeps the tab stop.
The arrows count from the lit row: with focus still on the menu the first arrow takes the row the pointer is on, and after that each goes on from whichever row is lit. When the pointer leaves the menu the row it lit goes dark, and the keyboard's row stays lit.
A submenu flies out beside its row the moment a pointer rests on it. It stays while the pointer moves diagonally toward it, across the rows below, because the triangle between the pointer and the flyout still counts as the row (data-rask-safe-area, Flux's safe area). It stays after the pointer has left the menu altogether, and closes when another row is entered.
While the arrows are inside a flyout its submenu's row stays lit with the row they are on. A tap opens a submenu on a touch screen, and the next one closes it. An open flyout is data-open on the submenu, written by the runtime like the rest.
Keep open
A pick closes the menu, a checkbox or a radio too, as in Flux. KeepOpen on the menu, a submenu, a radio group or a single row keeps it up, for the menu someone ticks several of.
Open state
Open is nullable, and its three settings mean three things. Unset leaves it to the reader; true and false hand it to the page, and the runtime shows or hides the popover to match whenever the page changes its mind, which is what lets a dropdown close itself when the action inside it completes: Ui.Dropdown.Open(_open).OnToggle(open => _open = open)[trigger, Ui.Menu[…]].
Rows, links and keys
The first child of a dropdown is the trigger: a Button, a Ui.Button, any element that is a button. The second is what it opens: a Ui.Menu of actions, a Ui.Navmenu of links, or a Ui.Popover.
A Ui.NavmenuItem is a link, with no menu roles and no cursor. Without an Href it is a <button> with OnClick; a string Href is a plain <a>, and a generated route navigates in the app.
Key can go anywhere in a row's chain, Ui.MenuCheckbox.Value(x).Key("k"), and a generic row such as Ui.MenuRadioGroup takes one too.
Style the rows from data-active and data-checked, and from data-open on the dropdown, its trigger and an open submenu.
Reference
Ui.Dropdown
A trigger, its first child, and what it opens, its second: a menu, a navigation menu or a popover.
Step | Takes | What it does |
|---|---|---|
.Position(…) | Ui.DropdownPositionBottom Top Right Left Each is a step of its own too, unless an HTML tag has its name. | Which side of the trigger it opens on. Below when unset. |
.Align(…) | Ui.DropdownAlignStart Center End Each is a step of its own too, unless an HTML tag has its name. | Where along that side it sits. Its start edge on the trigger's when unset. |
.Gap(…) | int | The distance from the trigger, in pixels. 5 when unset. |
.Offset(…) | int | Shifts it along its alignment, in pixels; negative moves it back past the trigger's edge. |
.Hover() | bool | Opens it while the pointer is over the trigger or the panel. A press and the keyboard still do. |
.Open() | bool | Hands the open state to the page. Unset, the reader opens and closes it. |
.OnToggle(…) | a handler given the state it is now in: open => … | Runs when the reader opens or closes it. |
Ui.Menu
The rows a dropdown or a context menu opens, with Flux's keyboard.
Step | Takes | What it does |
|---|---|---|
.KeepOpen() | bool | Keeps it open after any row in it is picked. |
Ui.MenuItem
An action. Its children are its words.
Step | Takes | What it does |
|---|---|---|
.Icon(…) | Ui.IconName — one of 317 names | An icon at the start of the row. |
.IconTrailing(…) | Ui.IconName — one of 317 names | An icon at the end of the row. |
.IconVariant(…) | Ui.IconVariantOutline Solid Mini Micro Each is a step of its own too, unless an HTML tag has its name. | Which drawing of the icons. Mini, 20px, when unset. |
.Kbd(…) | string | A shortcut shown at the end of the row: "⌘S". A hint: the page binds the key. |
.Suffix(…) | string | Words at the end of the row: a count, a state. |
.Variant(…) | Ui.MenuItemVariantDefault Danger Each is a step of its own too, unless an HTML tag has its name. | Danger for a destructive action. |
.Disabled() | bool | Shown but not pickable. The keyboard steps over it. |
.KeepOpen() | bool | Keeps the menu open after this row is picked. |
.Href(…) | a route, or a URL string | Where it leads. A generated route stays in the app; a string is a link the browser follows. |
.OnClick(…) | a handler, with no argument | What picking it does. |
Ui.MenuSubmenu
A row that flies out a menu of its own beside it.
Step | Takes | What it does |
|---|---|---|
.Heading(…) | string | The row's words. Required, and taken first. |
.Icon(…) | Ui.IconName — one of 317 names | An icon at the start of the row. |
.IconTrailing(…) | Ui.IconName — one of 317 names | The icon at the end of the row. A chevron when unset. |
.IconVariant(…) | Ui.IconVariantOutline Solid Mini Micro Each is a step of its own too, unless an HTML tag has its name. | Which drawing of the icons. Mini when unset. |
.KeepOpen() | bool | Keeps the menu open after any row in it is picked. |
Ui.MenuSeparator
A line between rows.
No steps of its own: what goes inside it is its content.
Ui.MenuGroup
Rows set apart from the rest, under a heading or between lines.
Step | Takes | What it does |
|---|---|---|
.Heading(…) | string | The words above the rows. Without it the group is set apart by lines alone. |
Ui.MenuCheckbox
A row that is on or off.
Step | Takes | What it does |
|---|---|---|
.Bind(…) | () => a bool | The model's flag it reads and writes: () => m.Draft. Or Value with OnChange. |
.Value(…) | bool | Whether it is ticked, for a page that holds the flag itself. |
.OnChange(…) | a handler given the new value | Runs with the new state when it is picked. |
.AfterBind(…) | a handler given the new value | Runs after Bind has written the model. |
.Validate(…) | a validator | A rule the value is checked by when it is written. |
.Live() | bool | Writes the model as soon as it is picked, Flux's wire:model.live, not with the next action. |
.Disabled() | bool | Shown but not pickable. |
.KeepOpen() | bool | Keeps the menu open after it is picked. |
Ui.MenuCheckboxGroup
Checkbox rows kept together. Each binds its own flag.
No steps of its own: what goes inside it is its content.
Ui.MenuRadioGroup
Radio rows of which one is picked. Its type is the type of their values.
Step | Takes | What it does |
|---|---|---|
.Bind(…) | () => a T | The model's value it reads and writes: () => m.Sort. Or Value with OnChange. |
.Value(…) | T, the type of the radios' values | The picked value, for a page that holds it itself. |
.OnChange(…) | a handler given the new value | Runs with the new value when a row is picked. |
.AfterBind(…) | a handler given the new value | Runs after Bind has written the model. |
.Validate(…) | a validator | A rule the value is checked by when it is written. |
.Live() | bool | Writes the model as soon as a row is picked, not with the next action. |
.KeepOpen() | bool | Keeps the menu open after a row in it is picked. |
Ui.MenuRadio
One choice in a radio group.
Step | Takes | What it does |
|---|---|---|
.Value(…) | a value of the group's type | The value it stands for, compared with the group's. |
.Checked() | bool | Whether it is the chosen one, for a radio outside a group. |
.Disabled() | bool | Shown but not pickable. |
.KeepOpen() | bool | Keeps the menu open after it is picked. |
.OnClick(…) | a handler, with no argument | Runs when it is picked, after its group has taken the value. |
Ui.Navmenu
A dropdown's list of links: no menu roles, no cursor, and Tab walks it.
No steps of its own: what goes inside it is its content.
Ui.NavmenuItem
One link. Its children are its words.
Step | Takes | What it does |
|---|---|---|
.Href(…) | a route, or a URL string | Where it leads. Without one the row is a button. |
.OnClick(…) | a handler, with no argument | What pressing a row with no Href does. |
.Icon(…) | Ui.IconName — one of 317 names | An icon at the start of the row. |
.IconVariant(…) | Ui.IconVariantOutline Solid Mini Micro Each is a step of its own too, unless an HTML tag has its name. | Which drawing of the icon. Mini when unset. |
.Variant(…) | Ui.MenuItemVariantDefault Danger Each is a step of its own too, unless an HTML tag has its name. | Danger for a destructive destination. |