Rask
Rask
v0.23.1-alpha.0.440

Components

Popover

Extra content in a panel beside a button, for what is not a menu. Ui.Popover is the second child of the Ui.Dropdown that holds its trigger, and where it opens is the dropdown's. The browser opens it and closes it on Escape and on a press anywhere else.

Drawn and behaving as Flux UI's popover does, measured against its live docs.

Popover

Focus stays on the trigger as it opens, and Tab walks from there into the panel. While it is up the page behind neither scrolls nor takes the pointer.

PopoverExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class PopoverExample : Component
{
    private string _sort = "posted";
    private string _view = "list";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            Ui.Dropdown[
                Ui.Button.Icon(Ui.IconName.AdjustmentsHorizontal).IconTrailing(Ui.IconName.ChevronDown)["Options"],
                Ui.Popover.Class("flex flex-col gap-4")[
                    Ui.RadioGroup.Value(_sort).Id("popover-sort").Label("Sort by").OnChange(v => { _sort = v; })[
                        Ui.Radio.Value("active").Label("Recently active"),
                        Ui.Radio.Value("posted").Label("Date posted")
                    ],
                    Ui.Separator.Key("s1").Subtle,
                    Ui.RadioGroup.Value(_view).Id("popover-view").Label("View as").OnChange(v => { _view = v; })[
                        Ui.Radio.Value("list").Label("List"),
                        Ui.Radio.Value("gallery").Label("Gallery")
                    ],
                    Ui.Separator.Key("s2").Subtle,
                    Ui.Button.Subtle.Sm.Class("justify-start -m-2 px-2!")
                        .OnClick(() => { _sort = "posted"; _view = "list"; })["Reset to default"]
                ]
            ],
            Ui.Text.Data("testid", "ui-popover-says")[$"Sorted by {_sort}, viewed as {_view}."]
        ];
}
Live result

Sorted by posted, viewed as list.

Hover trigger

Hover() on the dropdown: the pointer opens it and leaving closes it. A press and Enter open it too, and it locks nothing.

PopoverHoverExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class PopoverHoverExample : Component
{
    protected override Component? Render() =>
        Ui.Dropdown.Hover().Bottom.Start.Offset(-16).Gap(10)[
            Button.Type(ButtonType.Button).Class("flex items-center gap-3")[
                Ui.Avatar.Sm.Name("Mara Lind"),
                Ui.Heading["Mara Lind"]
            ],
            Ui.Popover.Class("flex flex-col gap-3 rounded-xl shadow-xl")[
                Ui.Avatar.Xl.Name("Mara Lind"),
                Div[
                    Ui.Heading.Lg["Mara Lind"],
                    Div.Class("flex items-center gap-2")[Ui.Text.Lg["@maralind"], Ui.Badge["Follows you"]]
                ],
                Div.Class("flex items-center gap-4")[
                    Div.Class("flex gap-1")[Ui.Heading["775"], Ui.Text["following"]],
                    Div.Class("flex gap-1")[Ui.Heading["50.2k"], Ui.Text["followers"]]
                ],
                Div.Class("flex gap-2")[
                    Ui.Button.Sm.Icon(Ui.IconName.Check).Class("flex-1")["Following"],
                    Ui.Button.Primary.Sm.Icon(Ui.IconName.ChatBubbleLeftRight).Class("flex-1")["Message"]
                ]
            ]
        ];
}
Live result
ML
Mara Lind

@maralind

Follows you
775

following

50.2k

followers

Position

The side the panel opens on, and where along it.

PopoverPositionExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class PopoverPositionExample : Component
{
    protected override Component? Render() =>
        Div.Class("grid max-w-xs grid-cols-2 gap-4")[
            Ui.Dropdown.Key("ts").Top.Start[
                Ui.Button.Filled.Sm.Class("w-full")["Top Start"],
                Ui.Popover[Ui.Text["Above, at the start of its button."]]
            ],
            Ui.Dropdown.Key("rc").Right.Center[
                Ui.Button.Filled.Sm.Class("w-full")["Right Center"],
                Ui.Popover[Ui.Text["To the right, centred on its button."]]
            ],
            Ui.Dropdown.Key("lc").Left.Center[
                Ui.Button.Filled.Sm.Class("w-full")["Left Center"],
                Ui.Popover[Ui.Text["To the left, centred on its button."]]
            ],
            Ui.Dropdown.Key("be").Bottom.End[
                Ui.Button.Filled.Sm.Class("w-full")["Bottom End"],
                Ui.Popover[Ui.Text["Below, at the end of its button."]]
            ]
        ];
}
Live result

Above, at the start of its button.

To the right, centred on its button.

To the left, centred on its button.

Below, at the end of its button.

Gap & offset

Gap is the distance from the trigger; Offset shifts the panel along its edge.

PopoverGapExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class PopoverGapExample : Component
{
    protected override Component? Render() =>
        Div.Class("grid max-w-sm grid-cols-2 gap-4")[
            Ui.Dropdown.Key("gap").Gap(16)[
                Ui.Button.Filled.Sm.Class("w-full")["Gap: 16px"],
                Ui.Popover[Ui.Text["16px from its button."]]
            ],
            Ui.Dropdown.Key("offset").Offset(32)[
                Ui.Button.Filled.Sm.Class("w-full")["Offset: 32px"],
                Ui.Popover[Ui.Text["Shifted 32px along its button."]]
            ]
        ];
}
Live result

16px from its button.

Shifted 32px along its button.

Examples

Category picker

A list of selectable categories, bound to the page. Picking keeps the panel open.

PopoverCategoriesExample.cs
using System.Globalization;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class PopoverCategoriesExample : Component
{
    private static readonly (string Value, string Label)[] Categories =
    [
        ("fantasy", "Fantasy"), ("science-fiction", "Science fiction"), ("horror", "Horror"), ("mystery", "Mystery"),
        ("romance", "Romance"), ("autobiography", "Autobiography"), ("thriller", "Thriller"), ("poetry", "Poetry"),
        ("children", "Children"),
    ];

    private List<string> _picked = ["fantasy", "science-fiction", "thriller"];

    protected override Component? Render() =>
        Ui.Dropdown[
            Ui.Button.Icon(Ui.IconName.Tag).IconTrailing(
                Ui.Badge.Sm.Class("-mr-1 tabular-nums").Data("testid", "ui-popover-count")[
                    _picked.Count.ToString(CultureInfo.InvariantCulture)
                ])["Categories"],
            Ui.Popover.Class("max-w-[18rem] flex flex-col gap-4")[
                Ui.CheckboxGroup.Values(_picked).Id("popover-categories").Variant(Ui.CheckboxGroupVariant.Pills)
                    .OnChange(v => { _picked = [.. v]; })[
                    Categories.Select(category => Ui.Checkbox.Key(category.Value).Value(category.Value).Label(category.Label))
                ],
                Ui.Separator.Subtle,
                Ui.Button.Subtle.Sm.Class("justify-start -m-2 px-2!").OnClick(() => { _picked = []; })["Clear all"]
            ]
        ];
}
Live result

Feedback

A form in a panel. The page owns Open, so the form's own buttons close it, and OnToggle is how the page hears Escape and a press outside.

PopoverFeedbackExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class PopoverFeedbackExample : Component
{
    private bool _open;
    private string _kind = "bug";
    private string _text = "";
    private string _sent = "nothing yet";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            // The page owns Open, so the form's own buttons can close it.
            Ui.Dropdown.Open(_open).OnToggle(open => { _open = open; })[
                Ui.Button.Icon(Ui.IconName.ChatBubbleOvalLeft)["Feedback"],
                Ui.Popover.Class("flex w-[min(30rem,calc(100vw-2rem))] flex-col gap-4")[
                    Ui.RadioGroup.Value(_kind).Id("popover-kind").Buttons.Class("max-sm:flex-wrap").OnChange(v => { _kind = v; })[
                        Ui.Radio.Value("bug").Icon(Ui.IconName.BugAnt).Class("flex-1")["Bug report"],
                        Ui.Radio.Value("suggestion").Icon(Ui.IconName.LightBulb).Class("flex-1")["Suggestion"],
                        Ui.Radio.Value("question").Icon(Ui.IconName.QuestionMarkCircle).Class("flex-1")["Question"]
                    ],
                    Ui.Textarea.Value(_text).Id("popover-feedback").Rows(8).Class("dark:bg-transparent!")
                        .Placeholder("Please include reproduction steps. You may attach images or video files.")
                        .OnInput(v => { _text = v; }),
                    Div.Class("flex justify-end gap-2")[
                        Ui.Button.Filled.Sm.Kbd("esc").Class("w-28").OnClick(() => { _open = false; })["Cancel"],
                        Ui.Button.Sm.Kbd("⏎").Class("w-28").OnClick(Send)["Submit"]
                    ]
                ]
            ],
            Ui.Text.Data("testid", "ui-popover-sent")[$"Last feedback: {_sent}."]
        ];

    private void Send()
    {
        _sent = _text.Length == 0 ? $"an empty {_kind}" : $"a {_kind}, “{_text}”";
        _text = "";
        _open = false;
    }
}
Live result

Last feedback: nothing yet.

Event details

Detailed information about an event, in a panel with no padding of its own.

PopoverEventExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class PopoverEventExample : Component
{
    private string _answer = "going";

    protected override Component? Render() =>
        // Centred on a phone: a panel centred under a trigger at the page's edge would hang off it.
        Div.Class("flex max-sm:justify-center")[
            Ui.Dropdown.Bottom.Center[
                Button.Type(ButtonType.Button)
                    .Class("flex w-54 items-center gap-2 rounded-lg bg-zinc-100 p-2 text-start hover:bg-zinc-200 "
                           + "dark:bg-zinc-700 dark:hover:bg-zinc-600")[
                    Div.Class("w-0.5 self-stretch rounded-full bg-zinc-800 dark:bg-zinc-200"),
                    Div[Ui.Heading["Team sync"], Ui.Text.Class("mt-1")["10:00 AM"]]
                ],
                // data-open:flex: an open panel says so, so its layout applies only while it is up.
                Ui.Popover.Class("w-80 p-0 data-open:flex flex-col")[
                    Div.Class("p-4")[
                        Ui.Heading["Team sync"],
                        Ui.Text.Class("mt-2")["Let's review the progress made last week and define the priorities for the next"],
                        Ui.RadioGroup.Value(_answer).Id("popover-answer").Segmented.Class("mt-3").OnChange(v => { _answer = v; })[
                            Ui.Radio.Value("going").Label("Going"),
                            Ui.Radio.Value("not-going").Label("Not going"),
                            Ui.Radio.Value("maybe").Label("Maybe")
                        ]
                    ],
                    Ui.Separator.Key("s1"),
                    Detail(Ui.IconName.Users, Ui.Heading["Guests"], "Invite",
                        Div.Class("mt-2 flex items-center gap-3")[
                            Ui.AvatarGroup.Class("dark:*:ring-zinc-700")[
                                Ui.Avatar.Xs.Circle().Name("Mara Lind"),
                                Ui.Avatar.Xs.Circle().Name("Theo Brandt"),
                                Ui.Avatar.Xs.Circle().Name("Nia Okafor")
                            ],
                            Ui.Text["1 maybe, 1 awaiting"]
                        ]),
                    Ui.Separator.Key("s2"),
                    Detail(Ui.IconName.Clock,
                        Div.Class("flex gap-2")[
                            Ui.Heading["10:00 AM"],
                            Ui.Icon.Name(Ui.IconName.ArrowRight).Micro.Class("text-zinc-400"),
                            Ui.Heading["11:00 AM"]
                        ],
                        "Edit",
                        Div.Class("mt-2 flex gap-3")[
                            Ui.Text["May 29 2025"],
                            Ui.Text["·"],
                            Div.Class("flex items-center gap-1.5")[
                                Ui.Icon.Name(Ui.IconName.ArrowPathRoundedSquare).Micro.Class("text-zinc-400"),
                                Ui.Text["Every weekday"]
                            ]
                        ]),
                    Ui.Separator.Key("s3"),
                    Detail(Ui.IconName.MapPin, Ui.Heading["Location"], "Edit",
                        Ui.Text.Class("mt-2")["Paris HQ, 13 rue de la Prairie 75018"])
                ]
            ]
        ];

    // One block of the event: an icon, and beside it a heading with a quiet link at its far edge, over a detail.
    private static Component Detail(Ui.IconName icon, Component heading, string link, Component detail) =>
        Div.Class("flex gap-2 p-4")[
            Ui.Icon.Name(icon).Micro.Class("text-zinc-400"),
            Div.Class("flex-1")[
                Div.Class("flex items-center justify-between")[
                    heading,
                    Ui.Text[Ui.Link.As(Ui.LinkAs.Button).Variant(Ui.LinkVariant.Subtle)[link]]
                ],
                detail
            ]
        ];
}
Live result
Team sync

Let's review the progress made last week and define the priorities for the next

Guests

ML
TB
NO

1 maybe, 1 awaiting

10:00 AM
11:00 AM

May 29 2025

·

Every weekday

Location

Paris HQ, 13 rue de la Prairie 75018

Behaviour

Opening and closing

For what is not a menu: a form, a set of filters, a card about a person. It is the second child of the Ui.Dropdown that holds its trigger, exactly as flux:popover sits in flux:dropdown, and everything about where it opens and what opens it is the dropdown's, its viewport placement too.

The panel is a [popover] the trigger names with popovertarget, so the browser opens it, lifts it into the top layer and closes it on Escape and on a press anywhere else. It is no menu and says no role.

Focus

Focus stays on the trigger as it opens: the panel takes none, and Tab walks from there into the panel. Shift+Tab off the trigger, or Tab past the panel's last control, closes it.

The page behind

While one a press opened is up, the page behind neither scrolls nor takes the pointer (data-rask-lock), so a press on anything else only closes it. One the pointer opened, Hover(), locks nothing; a press and Enter still open it too.

Layout while open

An open panel carries data-open from the moment it opens. The runtime writes it in the browser (data-rask-open-mark), so no round trip stands between the press and the layout, and it is what Flux's own data-open:flex styles by. A closed panel is never displayed, whatever layout its Class gives it.

From Flux

Flux's attributes, as steps:

Flux
What happens
flux:dropdown position="top" align="end"Ui.Dropdown.Top.End
flux:dropdown gap="16" offset="32"Ui.Dropdown.Gap(16).Offset(32)
flux:dropdown hoverUi.Dropdown.Hover(): the pointer opens it; a press and Enter still do.
flux:dropdown wire:model="open"Ui.Dropdown.Open(_open).OnToggle(open => _open = open)
flux:popover class="w-80 p-0"Ui.Popover.Class("w-80 p-0"): its own width, padding, radius and shadow weigh nothing, so yours win.

Reference

Ui.Popover

flux:popover

The panel a Ui.Dropdown opens beside its trigger, for what is not a menu. Where it opens is the dropdown's.

No steps of its own: what goes inside it is its content.