Components
Kanban
Cards arranged in columns, one column per stage of a workflow. It draws the board and moves nothing, as Flux's does: adding or moving a card is the page changing its own lists.
Drawn and behaving as Flux UI's kanban does, measured against its live docs.
Kanban
A column per stage, each with its name, a count and its cards.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class KanbanExample : Component
{
private static readonly (string Stage, string[] Cards)[] Board =
[
("Planned", ["Update privacy policy in app", "Search bar suggestions broken", "Improve loading spinner visuals", "Date picker keyboard input fails"]),
("In Progress", ["Mobile responsive improvements", "Data table sorting broken", "API error codes inconsistent"]),
("In review", ["Button double-click issue", "Crash on large file upload"]),
];
// A column does not shrink, so a board wider than its place scrolls in a box of its own.
protected override Component? Render() =>
Div.Class("overflow-x-auto pb-2")[
Ui.Kanban[
Board.Select(column => Ui.KanbanColumn.Key(column.Stage)[
Ui.KanbanColumnHeader.Heading(column.Stage).Count(column.Cards.Length),
Ui.KanbanColumnCards[column.Cards.Select(card => Ui.KanbanCard.Key(card).Heading(card))]
])
]
];
}Column actions
Buttons at the end of the heading's row. The plus adds a card to the page's list.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class KanbanActionsExample : Component
{
private readonly List<string> _planned = ["Search bar suggestions broken", "Improve loading spinner visuals"];
protected override Component? Render() =>
Ui.KanbanColumn[
Ui.KanbanColumnHeader.Heading("Planned").Count(_planned.Count).Actions([
Ui.Button.Subtle.Sm.Icon(Ui.IconName.EllipsisHorizontal).AriaLabel("Column options"),
Ui.Button.Subtle.Sm.Icon(Ui.IconName.Plus).AriaLabel("New card").OnClick(() => _planned.Add($"Card {_planned.Count + 1}"))
]),
Ui.KanbanColumnCards[_planned.Select(card => Ui.KanbanCard.Key(card).Heading(card))]
];
}Subheading
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class KanbanSubheadingExample : Component
{
protected override Component? Render() =>
Ui.KanbanColumn[
Ui.KanbanColumnHeader.Heading("Backlog").Subheading("Ideas and suggestions"),
Ui.KanbanColumnCards[
Ui.KanbanCard.Heading("Dark mode for the invoice PDF"),
Ui.KanbanCard.Heading("Keyboard shortcuts cheat sheet")
]
];
}Column footer
A form under the cards that adds one, and the count follows.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class KanbanFooterExample : Component
{
private readonly List<string> _planned = ["Search bar suggestions broken", "Improve loading spinner visuals"];
private readonly NewCard _newCard = new();
protected override Component? Render() =>
Ui.KanbanColumn[
Ui.KanbanColumnHeader.Heading("Planned").Count(_planned.Count),
Ui.KanbanColumnCards[_planned.Select(card => Ui.KanbanCard.Key(card).Heading(card))],
Ui.KanbanColumnFooter[
Form.Model(_newCard).OnSubmit(Add)[
Ui.KanbanCard[
Div.Class("flex items-center gap-1")[
Ui.Heading.Class("flex-1")[
Input.Bind(() => _newCard.Title).Class("w-full outline-none").Placeholder("New card...").AriaLabel("New card")
],
Ui.Button.Filled.Sm.Type(Ui.ButtonType.Submit).Inset(Ui.Inset.Top | Ui.Inset.Bottom).Class("-me-1.5")["Add"]
]
]
],
Ui.Button.Subtle.Sm.Icon(Ui.IconName.Plus).Align(Ui.Align.Start)
.OnClick(() => _planned.Add($"Card {_planned.Count + 1}"))["New card"]
]
];
private void Add(NewCard card)
{
if (string.IsNullOrWhiteSpace(card.Title))
{
return;
}
_planned.Add(card.Title.Trim());
card.Title = "";
}
private sealed class NewCard
{
public string Title { get; set; } = "";
}
}Cards as buttons
As(Ui.KanbanCardAs.Button) makes the whole card one to press, with badges above its heading or an icon under it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class KanbanCardButtonsExample : Component
{
private string? _opened;
protected override Component? Render() =>
Div.Class("space-y-3")[
Ui.KanbanColumn[
Ui.KanbanColumnHeader.Heading("Planned"),
Ui.KanbanColumnCards[
Card("plain", "Update privacy policy in app"),
Card("header", "Search bar suggestions broken").Header(
Div.Class("flex gap-2")[
Ui.Badge.Sm.Color(Ui.Color.Blue)["UI"],
Ui.Badge.Sm.Color(Ui.Color.Green)["Backend"],
Ui.Badge.Sm.Color(Ui.Color.Red)["Bug"]
]),
Card("footer", "Improve loading spinner visuals").Footer([
Ui.Icon.Name(Ui.IconName.Bars3BottomLeft).Variant(Ui.IconVariant.Micro).Class("text-zinc-400"),
Ui.Text.Inline()["3 people"]
])
]
],
Ui.Text.Data("testid", "ui-kanban-opened")[_opened is null ? "No card opened yet." : $"Opened: {_opened}"]
];
private UiKanbanCard Card(string key, string title) =>
Ui.KanbanCard.Key(key).As(Ui.KanbanCardAs.Button).Heading(title).OnClick(() => { _opened = title; });
}No card opened yet.
Behaviour
A card is something to read
A card is a <div> until .As(Ui.KanbanCardAs.Button) makes the whole card a <button>: focused with Tab, pressed with Enter or Space, lighter under the pointer. OnClick is what it does. Header goes above the heading (badges), Footer under it (an icon, avatars), and children replace the heading.
It moves nothing
Exactly as Flux's: no card is draggable, no key reorders one, and there is no drop event. Adding or moving a card is your page changing its own lists and rendering again; for dragging, DragDrop is the framework's primitive.
Fitting a board in
A column does not shrink, so a board wider than its place needs a box that scrolls: Div.Class("overflow-x-auto")[Ui.Kanban[…]].
Flux's badge prop on the column header is not carried: no example on Flux's page draws it, so there is nothing to measure it against. A Ui.Badge of your own goes in as a child, beside the heading you write there.
Reference
Ui.Kanban
The row the columns stand in, 16px apart. It draws a board and moves nothing.
No steps of its own: what goes inside it is its content.
It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.
Ui.KanbanColumn
One stage: a 320px tinted panel as tall as what is in it.
No steps of its own: what goes inside it is its content.
It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.
Ui.KanbanColumnHeader
The stage's name and what goes beside it. Children replace the heading and count.
Step | Takes | What it does |
|---|---|---|
.Heading(…) | string | What the column is called. |
.Subheading(…) | string | A second line under the heading. |
.Count(…) | int | A number beside the heading: how many cards the column holds. |
.Actions(…) | Component | Buttons or a menu at the end of the heading's row. |
It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.
Ui.KanbanColumnCards
The column's cards, one under the other.
No steps of its own: what goes inside it is its content.
It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.
Ui.KanbanColumnFooter
What sits under the cards: a New card button, a form.
No steps of its own: what goes inside it is its content.
It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.
Ui.KanbanCard
A card. Children replace the heading.
Step | Takes | What it does |
|---|---|---|
.Heading(…) | string | The card's title. |
.As(…) | Ui.KanbanCardAsDiv Button Each is a step of its own too, unless an HTML tag has its name. | The element it is: Button makes the whole card one to press. A div when unset. |
.Header(…) | Component | What sits above the heading: badges, tags. |
.Footer(…) | Component | What sits under the heading: an icon, avatars. |
It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.