ExamplesForm Inputs

#Form InputsGitHub

Every kind of form control, each bound to signals and running live above its code. Text-like fields bind their value with ToDomEvent; checkboxes, radio buttons and files read the element the event came from, through the typed DOM. Each listing is the whole component, minus labels and styling. Its first render came from the server, so the values you see before you type are the ones in the code.

#Demo

#Text<input type="text">

ToDomEvent turns a signal into an input handler that writes the field's value into it. Passing the same signal back as Value keeps the field and the state in step, in both directions.

Live

Hello, stranger.

NameField.cs
public class NameField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var name = new Signal<string>("");
        var greeting = new Computed<string>(() => name.Value.Trim() is { Length: > 0 } trimmed
            ? $"Hello, {trimmed}!"
            : "Hello, stranger.");

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "text".ToConstSignal(),
                    Placeholder = "Your name".ToConstSignal(),
                    Value = name,
                },
                // Writes every keystroke into the signal.
                Events = new InputEvents { OnInput = name.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = greeting }],
            },
        ];
    }
}

#Password<input type="password">

Props are signals too, so a Computed can drive the input's type: one click flips it between password and text. The strength score is plain C# over the value.

Live

Empty

Try a capital, a digit and a symbol.

PasswordField.cs
public class PasswordField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var password = new Signal<string>("");
        var visible = new Signal<bool>(false);
        var strength = new Computed<string>(() => Score(password.Value) switch
        {
            0 => "Empty",
            1 => "Weak",
            2 => "Fair",
            3 => "Good",
            _ => "Strong",
        });

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    // Swapping the type shows or hides what was typed.
                    Type = new Computed<string>(() => visible.Value ? "text" : "password"),
                    Value = password,
                },
                Events = new InputEvents { OnInput = password.ToDomEvent() },
            },
            new Button
            {
                Props = new ButtonProps(),
                Events = new ButtonEvents { OnClick = _ => visible.Value = !visible.Value },
                Children = [new DomText { Text = new Computed<string>(
                    () => visible.Value ? "Hide" : "Show") }],
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = strength }],
            },
        ];
    }

    // One point for being long enough, and one each for a capital, a digit and a symbol.
    private static int Score(string value) => value.Length == 0 ? 0 :
        1 + (value.Length >= 8 && value.Any(char.IsUpper) ? 1 : 0)
          + (value.Any(char.IsDigit) ? 1 : 0)
          + (value.Any(c => !char.IsLetterOrDigit(c)) ? 1 : 0);
}

#Email<input type="email">

The browser already knows what a valid address looks like. The handler reads the value and the browser's own validation message off the typed HTMLInputElement.

Live

Waiting for an address

EmailField.cs
public class EmailField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var email = new Signal<string>("");
        var error = new Signal<string>("");
        var status = new Computed<string>(() =>
            email.Value.Length == 0 ? "Waiting for an address"
            : error.Value.Length == 0 ? $"✓ {email.Value}"
            : $"✗ {error.Value}");

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "email".ToConstSignal(),
                    Placeholder = "you@example.com".ToConstSignal(),
                    Value = email,
                },
                Events = new InputEvents
                {
                    OnInput = e =>
                    {
                        if (OperatingSystem.IsBrowser() && e.Target is HTMLInputElement input)
                        {
                            email.Value = input.Value;
                            // The browser checks the address; its message says what's wrong.
                            error.Value = input.ValidationMessage;
                        }
                    },
                },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = status }],
            },
        ];
    }
}

#URL<input type="url">

The value is parsed with System.Uri on every keystroke: the same .NET code that rendered the first result on the server.

Live

https · github.com · /miroljub1995/natrix · ?tab=readme

UrlField.cs
public class UrlField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var url = new Signal<string>("https://github.com/miroljub1995/natrix?tab=readme");

        // System.Uri does the parsing: the same .NET on the server and in the browser.
        var parts = new Computed<string>(() =>
            Uri.TryCreate(url.Value, UriKind.Absolute, out var uri)
                ? $"{uri.Scheme} · {uri.Host} · {uri.AbsolutePath} · {uri.Query}"
                : "Not an absolute URL");

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "url".ToConstSignal(),
                    Value = url,
                },
                Events = new InputEvents { OnInput = url.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = parts }],
            },
        ];
    }
}

#Telephone<input type="tel">

A tel field is free text, but on phones it brings up the dial pad. The formatting lives in a Computed, which runs again whenever the signal changes.

Live

0 of 10 digits

On a phone, this field opens the dial pad.

PhoneField.cs
public class PhoneField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var phone = new Signal<string>("");
        var formatted = new Computed<string>(() =>
        {
            var digits = new string([.. phone.Value.Where(char.IsDigit)]);
            return digits.Length == 10
                ? $"({digits[..3]}) {digits[3..6]}-{digits[6..]}"
                : $"{digits.Length} of 10 digits";
        });

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "tel".ToConstSignal(),
                    Placeholder = "(555) 123-4567".ToConstSignal(),
                    Value = phone,
                },
                Events = new InputEvents { OnInput = phone.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = formatted }],
            },
        ];
    }
}

#Number<input type="number">

Min, Max and Step constrain the spinner. The value still arrives as a string, so it is parsed where it is read, and anything out of range is caught there.

Live

3 × $12 = $36

TicketField.cs
public class TicketField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    private const int TicketPrice = 12;

    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        // The field's value is always a string; parse it where it's read.
        var quantity = new Signal<string>("3");
        var total = new Computed<string>(() =>
            int.TryParse(quantity.Value, out var n) && n is >= 1 and <= 20
                ? $"{n} × ${TicketPrice} = ${n * TicketPrice}"
                : "Pick between 1 and 20");

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "number".ToConstSignal(),
                    Min = "1".ToConstSignal(),
                    Max = "20".ToConstSignal(),
                    Step = "1".ToConstSignal(),
                    Value = quantity,
                },
                Events = new InputEvents { OnInput = quantity.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = total }],
            },
        ];
    }
}

#Range<input type="range">

OnInput fires as the slider moves, not just when it is released. A Computed style updates the one attribute that depends on it.

Live

Aa · 24px

FontSizeSlider.cs
public class FontSizeSlider : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var size = new Signal<string>("24");

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "range".ToConstSignal(),
                    Min = "12".ToConstSignal(),
                    Max = "64".ToConstSignal(),
                    Value = size,
                },
                // Input fires while dragging; Change only on release.
                Events = new InputEvents { OnInput = size.ToDomEvent() },
            },
            new P
            {
                // Only this one attribute is written as the slider moves.
                Props = new PProps { Style = new Computed<string>(() => $"font-size: {size.Value}px") },
                Children = [new DomText { Text = new Computed<string>(() => $"Aa · {size.Value}px") }],
            },
        ];
    }
}

#Checkbox<input type="checkbox">

A checkbox's state is a bool, so it binds through Checked and OnChange rather than through its value. The button stays disabled until the box is ticked.

Live
TermsCheckbox.cs
public class TermsCheckbox : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var agreed = new Signal<bool>(false);
        var signedUp = new Signal<bool>(false);

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "checkbox".ToConstSignal(),
                    Checked = agreed,
                },
                Events = new InputEvents
                {
                    // A checkbox has no text value to bind: read Checked off the element.
                    OnChange = e =>
                    {
                        if (OperatingSystem.IsBrowser() && e.Target is HTMLInputElement input)
                        {
                            agreed.Value = input.Checked;
                            signedUp.Value = false;
                        }
                    },
                },
            },
            new Button
            {
                Props = new ButtonProps { Disabled = new Computed<bool>(() => !agreed.Value) },
                Events = new ButtonEvents { OnClick = _ => signedUp.Value = true },
                Children = [new DomText { Text = new Computed<string>(
                    () => signedUp.Value ? "Signed up ✓" : "Sign up") }],
            },
        ];
    }
}

#Radio<input type="radio">

Radio buttons that share a name form a group. Each one is checked while the selected plan is its own, and choosing one updates the selection.

Live
Plan

Pro: $12 a month

PlanPicker.cs
public class PlanPicker : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    private record Plan(string Id, string Name, int Price);

    private static readonly Plan[] Plans =
        [new("free", "Free", 0), new("pro", "Pro", 12), new("team", "Team", 49)];

    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var selected = new Signal<string>("pro");
        var summary = new Computed<string>(() =>
            Plans.Single(p => p.Id == selected.Value) is var plan && plan.Price == 0
                ? $"{plan.Name}: free forever"
                : $"{plan.Name}: ${plan.Price} a month");

        return
        [
            // One shared name makes them a group: checking one unchecks the rest.
            .. Plans.Select(plan => new Input
            {
                Props = new InputProps
                {
                    Type = "radio".ToConstSignal(),
                    Name = "plan".ToConstSignal(),
                    Value = plan.Id.ToConstSignal(),
                    Checked = new Computed<bool>(() => selected.Value == plan.Id),
                },
                Events = new InputEvents { OnChange = _ => selected.Value = plan.Id },
            }),
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = summary }],
            },
        ];
    }
}

#Color<input type="color">

The browser's colour picker always reports "#rrggbb". Convert.FromHexString splits that into channels for the rgb() readout.

Live
#6366f1 · rgb(99, 102, 241)
ColorPicker.cs
public class ColorPicker : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        // Always "#rrggbb", lowercase.
        var color = new Signal<string>("#6366f1");
        var rgb = new Computed<string>(() =>
        {
            var value = Convert.FromHexString(color.Value[1..]);
            return $"rgb({value[0]}, {value[1]}, {value[2]})";
        });

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "color".ToConstSignal(),
                    Value = color,
                },
                Events = new InputEvents { OnInput = color.ToDomEvent() },
            },
            new Div
            {
                Props = new DivProps
                {
                    Style = new Computed<string>(() => $"background-color: {color.Value}"),
                },
                Children = [new DomText { Text = new Computed<string>(() => $"{color.Value} · {rgb.Value}") }],
            },
        ];
    }
}

#Date<input type="date">

The picker shows the date in the reader's locale, but the value is always ISO "yyyy-MM-dd". DateOnly parses it the same way on the server and in the browser.

Live

Saturday, July 4 · day 185 of 2026

DateField.cs
public class DateField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        // Always "yyyy-MM-dd", whatever the browser shows, or "" while incomplete.
        var date = new Signal<string>("2026-07-04");
        var summary = new Computed<string>(() => Describe(date.Value));

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "date".ToConstSignal(),
                    Value = date,
                },
                Events = new InputEvents { OnInput = date.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = summary }],
            },
        ];
    }

    private static string Describe(string value) =>
        DateOnly.TryParse(value, CultureInfo.InvariantCulture, out var date)
            ? $"{date.ToString("dddd, MMMM d", CultureInfo.InvariantCulture)} · day {date.DayOfYear} of {date.Year}"
            : "Pick a date";
}

#Time<input type="time">

The value is 24-hour "HH:mm", even where the picker shows AM and PM. TimeOnly does the arithmetic.

Live

9:30 AM · 14h 30m before midnight

TimeField.cs
public class TimeField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        // Always 24-hour "HH:mm", even where the browser shows AM and PM.
        var time = new Signal<string>("09:30");
        var summary = new Computed<string>(() => Describe(time.Value));

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "time".ToConstSignal(),
                    Value = time,
                },
                Events = new InputEvents { OnInput = time.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = summary }],
            },
        ];
    }

    private static string Describe(string value)
    {
        if (!TimeOnly.TryParse(value, CultureInfo.InvariantCulture, out var time))
        {
            return "Pick a time";
        }

        var left = TimeSpan.FromDays(1) - time.ToTimeSpan();
        return $"{time.ToString("h:mm tt", CultureInfo.InvariantCulture)} · {left.Hours}h {left.Minutes}m before midnight";
    }
}

#Date and time<input type="datetime-local">

A date and a time in one field, with no time zone attached. DateTime parses it, and the end of a 45-minute meeting follows.

Live

Thu, Dec 31, 2026 at 23:30 → ends Fri 00:15

MeetingField.cs
public class MeetingField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        // "yyyy-MM-ddTHH:mm", with no time zone: it means the time on the reader's clock.
        var start = new Signal<string>("2026-12-31T23:30");
        var summary = new Computed<string>(() => Describe(start.Value));

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "datetime-local".ToConstSignal(),
                    Value = start,
                },
                Events = new InputEvents { OnInput = start.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = summary }],
            },
        ];
    }

    private static string Describe(string value) =>
        DateTime.TryParse(value, CultureInfo.InvariantCulture, out var start)
            ? $"{start.ToString("ddd, MMM d, yyyy 'at' HH:mm", CultureInfo.InvariantCulture)} → ends {start.AddMinutes(45).ToString("ddd HH:mm", CultureInfo.InvariantCulture)}"
            : "Pick a date and time";
}

#Month<input type="month">

A year and a month with no day, as "yyyy-MM". Chromium browsers show a month picker; Firefox and Safari fall back to a text field.

Live

February 2026 · 28 days, starts on a Sunday

MonthField.cs
public class MonthField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        // "yyyy-MM": a month with no day.
        var month = new Signal<string>("2026-02");
        var summary = new Computed<string>(() => Describe(month.Value));

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "month".ToConstSignal(),
                    Value = month,
                },
                Events = new InputEvents { OnInput = month.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = summary }],
            },
        ];
    }

    private static string Describe(string value) =>
        DateOnly.TryParse($"{value}-01", CultureInfo.InvariantCulture, out var first)
            ? $"{first.ToString("MMMM yyyy", CultureInfo.InvariantCulture)} · {DateTime.DaysInMonth(first.Year, first.Month)} days, starts on a {first.DayOfWeek}"
            : "Pick a month";
}

#Week<input type="week">

An ISO 8601 week, as "yyyy-Www". ISOWeek turns it into the Monday it starts on. Like month, Firefox and Safari show a text field.

Live

Week 27: Mon Jun 29 – Sun Jul 5, 2026

WeekField.cs
public class WeekField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        // "yyyy-Www", an ISO 8601 week, which .NET's ISOWeek understands.
        var week = new Signal<string>("2026-W27");
        var summary = new Computed<string>(() => Describe(week.Value));

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "week".ToConstSignal(),
                    Value = week,
                },
                Events = new InputEvents { OnInput = week.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = summary }],
            },
        ];
    }

    private static string Describe(string value)
    {
        var parts = value.Split("-W");
        if (parts.Length != 2 || !int.TryParse(parts[0], out var year) || !int.TryParse(parts[1], out var week)
            || week < 1 || week > ISOWeek.GetWeeksInYear(year))
        {
            return "Pick a week";
        }

        var monday = DateOnly.FromDateTime(ISOWeek.ToDateTime(year, week, DayOfWeek.Monday));
        return $"Week {week}: {monday.ToString("ddd MMM d", CultureInfo.InvariantCulture)} – {monday.AddDays(6).ToString("ddd MMM d, yyyy", CultureInfo.InvariantCulture)}";
    }
}

#File<input type="file">

Multiple lets the reader pick several files. The handler reads their names and sizes from the input's FileList; nothing is uploaded.

Live

No files yet

FilePicker.cs
public class FilePicker : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    private record PickedFile(string Name, ulong Size);

    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var files = new Signal<IReadOnlyList<PickedFile>>([]);
        var total = new Computed<string>(() => files.Value.Count == 0
            ? "No files yet"
            : $"{files.Value.Count} files, {files.Value.Sum(f => (double)f.Size) / 1024:0.#} KB");

        return
        [
            new Input
            {
                Props = new InputProps
                {
                    Type = "file".ToConstSignal(),
                    Multiple = true.ToConstSignal(),
                },
                Events = new InputEvents
                {
                    OnChange = e =>
                    {
                        // Nothing leaves the browser: this only reads names and sizes.
                        if (OperatingSystem.IsBrowser() && e.Target is HTMLInputElement { Files: { } list })
                        {
                            var picked = new List<PickedFile>();
                            for (uint i = 0; i < list.Length; i++)
                            {
                                var file = list.Item(i)!;
                                picked.Add(new PickedFile(file.Name, file.Size));
                            }

                            files.Value = picked;
                        }
                    },
                },
            },
            new Ul
            {
                Props = new UlProps(),
                Children =
                [
                    new ForEach<PickedFile, string>
                    {
                        Items = files,
                        Key = file => file.Name,
                        ElementSetup = file =>
                        [
                            new Li { Props = new LiProps(), Children = [new DomText {
                                Text = new Computed<string>(() => file.Value.Name) }] },
                        ],
                    },
                ],
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = total }],
            },
        ];
    }
}

#Text area<textarea>

Multi-line text binds exactly like an input. MaxLength stops typing at the limit, and the counters update as you write.

Live

5 words · 114 characters left

BioField.cs
public class BioField : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    private const int Limit = 140;

    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var bio = new Signal<string>("Writes C# for the browser.");
        var words = new Computed<int>(() =>
            bio.Value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length);

        return
        [
            new TextArea
            {
                Props = new TextAreaProps
                {
                    Rows = 4u.ToConstSignal(),
                    MaxLength = Limit.ToConstSignal(),
                    Value = bio,
                },
                // The same binding as a text input: ToDomEvent knows both.
                Events = new TextAreaEvents { OnInput = bio.ToDomEvent() },
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = new Computed<string>(
                    () => $"{words.Value} words · {Limit - bio.Value.Length} characters left") }],
            },
        ];
    }
}

#Select<select>

Value selects the matching option, on the server as well, so the page arrives with Japan already chosen.

Live

The capital of Japan is Tokyo.

CountryPicker.cs
public class CountryPicker : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    private static readonly Dictionary<string, string> Capitals = new()
    {
        ["Brazil"] = "Brasília",
        ["Canada"] = "Ottawa",
        ["Japan"] = "Tokyo",
        ["Kenya"] = "Nairobi",
        ["Serbia"] = "Belgrade",
    };

    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var country = new Signal<string>("Japan");

        return
        [
            new Select
            {
                // Selects the option with this value, on the server too.
                Props = new SelectProps { Value = country },
                Events = new SelectEvents { OnChange = country.ToDomEvent() },
                Children =
                [
                    .. Capitals.Keys.Select(name => new Option
                    {
                        Props = new OptionProps { Value = name.ToConstSignal() },
                        Children = [new DomText { Text = name.ToConstSignal() }],
                    }),
                ],
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = new Computed<string>(
                    () => $"The capital of {country.Value} is {Capitals[country.Value]}.") }],
            },
        ];
    }
}

#Multiple select<select multiple>

With Multiple, bind Values to a list instead of Value to a string. ToDomEvent has an overload for lists that collects every selected option.

Live

Pizza with Basil, Olives: $10

Ctrl or ⌘ click to pick more than one.

ToppingsPicker.cs
public class ToppingsPicker : BaseComponent<NoProps, NoEvents, NoSlots, NoExpose>
{
    private static readonly string[] Toppings =
        ["Basil", "Mushrooms", "Olives", "Onions", "Peppers", "Pineapple"];

    protected override IComponent[] Setup(out NoExpose exposed)
    {
        exposed = default;

        var chosen = new Signal<IReadOnlyList<string>>(["Basil", "Olives"]);
        var order = new Computed<string>(() => chosen.Value.Count == 0
            ? "Margherita, nothing on top"
            : $"Pizza with {string.Join(", ", chosen.Value)}: ${8 + chosen.Value.Count}");

        return
        [
            new Select
            {
                Props = new SelectProps
                {
                    Multiple = true.ToConstSignal(),
                    Size = 6u.ToConstSignal(),
                    // Values, not Value: every selected option, as a list.
                    Values = chosen,
                },
                Events = new SelectEvents { OnChange = chosen.ToDomEvent() },
                Children =
                [
                    .. Toppings.Select(topping => new Option
                    {
                        Props = new OptionProps { Value = topping.ToConstSignal() },
                        Children = [new DomText { Text = topping.ToConstSignal() }],
                    }),
                ],
            },
            new P
            {
                Props = new PProps(),
                Children = [new DomText { Text = order }],
            },
        ];
    }
}