Files
Htmx/docs/Components/RadioGroup.md
T
2026-05-04 19:57:48 +05:00

6.0 KiB
Raw Blame History

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 RadioGroup submits 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 htmx attribute 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 htmx attribute 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")]