ee8797c142
Co-authored-by: Copilot <copilot@github.com>
6.0 KiB
6.0 KiB
RadioGroup
A group of radio buttons sharing the same name attribute. Supports horizontal or vertical layout. One option can be pre-selected.
HTML structure
div.flex.flex-col.gap-1.5
label.text-sm.font-medium ← group label (omitted when empty)
{label}
div.flex.{direction}.gap-3 ← flex-col or flex-row
label.flex.items-center.gap-2.cursor-pointer ← one per option
input[type=radio, name, value, class, $$Checked$$]
span.text-sm
{option label}
CSS mechanics
| Class | Effect |
|---|---|
accent-primary |
Radio circle color follows --color-primary CSS variable |
h-4 w-4 |
16×16 radio circle |
cursor-pointer |
Pointer cursor on the label |
flex-col (default) |
Stacks options vertically |
flex-row |
Places options side by side |
Constructor signature
public RadioGroup(
string name,
IEnumerable<(string Value, string Label, bool Selected)> options,
string label = "",
string direction = "flex-col")
| Parameter | Description |
|---|---|
name |
Shared name attribute for all radio inputs in the group |
options |
List of (Value, Label, Selected) tuples |
label |
Optional visible group heading above the options |
direction |
"flex-col" (vertical, default) or "flex-row" (horizontal) |
Usage examples
Vertical list
new RadioGroup(
name: "plan",
label: "Select a plan",
options: new[]
{
("free", "Free", true),
("pro", "Pro", false),
("teams", "Teams", false),
})
Horizontal inline options
new RadioGroup(
name: "size",
label: "Size",
direction: "flex-row",
options: new[]
{
("sm", "S", false),
("md", "M", true),
("lg", "L", false),
("xl", "XL", false),
})
Reading in a form handler
public record Command([property: FromForm] string Plan);
// command.Plan == "free" | "pro" | "teams"
Dynamic options from database
var options = categories
.Select((cat, i) => (cat.Slug, cat.Name, i == 0))
.ToArray();
new RadioGroup(name: "category", label: "Category", options: options)
Tips and tricks
- Only one option in the group can have
Selected = true; if multiple are marked selected the last one wins (standard HTML behavior). - An unselected
RadioGroupsubmits nothing — validate server-side that the field is present. - For a "none of the above" option, add a tuple with the intended empty value:
("", "None", false). - To conditionally show additional fields when a radio is selected, add an
htmxattribute via inline HTML after the component — or use a custom slot that includes both the radio and a reveal div. - To conditionally show additional fields when a radio is selected, add an
htmxattribute via inline HTML after the component — or use a custom slot that includes both the radio and a reveal div.
Complete page example
Templates/SurveyPage.htmx
<div class="max-w-md mx-auto py-10">
<h1 class="text-2xl font-bold mb-2">Quick survey</h1>
<p class="text-sm text-muted-foreground mb-8">Help us improve BeepBoop.</p>
<form method="post" action="/survey">
$$AntiforgeryToken$$
<div class="space-y-8 mb-8">
$$ExperienceGroup$$
$$FeatureGroup$$
</div>
$$SubmitBtn$$
</form>
$$SuccessAlert$$
</div>
Templates/SurveyPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class SurveyPage : SurveyPageBase
{
private readonly IHtmxComponent _experience;
private readonly IHtmxComponent _feature;
private readonly IHtmxComponent _submit;
private readonly IHtmxComponent _success;
private readonly byte[] _afToken;
public SurveyPage(IAntiforgery af, HttpContext ctx, bool submitted = false)
{
var tokens = af.GetAndStoreTokens(ctx);
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
_experience = new Components.RadioGroup(
name: "experience",
label: "How would you rate your experience?",
options: new[]
{
("1", "Poor", false),
("2", "Fair", false),
("3", "Good", true),
("4", "Very good", false),
("5", "Excellent", false),
},
direction: "flex-row");
_feature = new Components.RadioGroup(
name: "favourite",
label: "Which feature do you use most?",
options: new[]
{
("htmx", "HTMX integration", false),
("aot", "AOT publishing", false),
("generator", "Source generator", false),
("tailwind", "Tailwind CSS", false),
});
_submit = new Components.Button("Submit", type: "submit");
_success = submitted
? new Components.Alert(title: "Thank you!", description: "Your responses have been recorded.")
: HtmxEmpty.Instance;
}
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
protected override void RenderExperienceGroup(HtmxRenderContext ctx) => _experience.Render(ctx.Next());
protected override void RenderFeatureGroup(HtmxRenderContext ctx) => _feature.Render(ctx.Next());
protected override void RenderSubmitBtn(HtmxRenderContext ctx) => _submit.Render(ctx.Next());
protected override void RenderSuccessAlert(HtmxRenderContext ctx) => _success.Render(ctx.Next());
}
POST handler
[Handler]
[MapPost("/survey")]
public static partial class PostSurveyHandler
{
public record Command(
[property: FromForm] string Experience,
[property: FromForm] string Favourite);
private static Task<IResult> HandleAsync(
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
{
// Persist responses…
return ctx.WriteHtmxPage(new SurveyPage(af, ctx, submitted: true), title: "Survey");
}
}
AppJsonSerializerContext.cs
[JsonSerializable(typeof(PostSurveyHandler.Command), TypeInfoPropertyName = "SurveyCommand")]