Repository: microsoft/fluentui-blazor
Pull request #3647: [dev-v5] Add Radio and RadioGroup 
Author: login: vnbaaij
Labels: name: v5
description: For the next major version
Created: 2025-04-08T21:46:43Z | Merged: 2025-04-15T06:53:00Z

This PR adds `FluentRadio` and `FluentRadioGroup` components. These are modeled after the standard Blazor `InputRadio` and `InputRadioGroup`

![image](https://github.com/user-attachments/assets/43493bb4-de51-4fe8-996b-16491daea738)

Fully tested:
![image](https://github.com/user-attachments/assets/99f3d51b-d15b-4818-abf9-9dfd03474666)


In earlier versions, `FluentRadio` inherited from `FluentComponentBase`. To make it work with the `FluentField` we use now, this has been changed so the component now inherits from `FluentinputBase`. Also, the `ChildComponent` parameter is no longer supported. You must use `Label` or `LabelTemplate` to set the label.


Diff:
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioDefault.razor
new file mode 100644
index 0000000000..b3125b820f
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioDefault.razor
@@ -0,0 +1,17 @@
+﻿<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">
+    <FluentRadioGroup TValue="string" Label="Default">
+        <FluentRadio></FluentRadio>
+    </FluentRadioGroup>
+
+    <FluentRadioGroup TValue="string" Label="Checked" Value="@(string.Empty)">
+        <FluentRadio Value="@(string.Empty)"></FluentRadio>
+    </FluentRadioGroup>
+
+    <FluentRadioGroup TValue="string" Label="Disabled">
+        <FluentRadio Disabled="true"></FluentRadio>
+    </FluentRadioGroup>
+
+    <FluentRadioGroup TValue="string" Label="Field">
+        <FluentRadio Label="Apple" Value="@("Apple")"></FluentRadio>
+    </FluentRadioGroup>
+</FluentStack>
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDefault.razor
new file mode 100644
index 0000000000..596f54c037
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDefault.razor
@@ -0,0 +1,13 @@
+﻿<FluentRadioGroup @bind-Value="@fruit" Label="Favorite fruit" Name="favorite-fruit">
+    <FluentRadio Value="@("apple")" Label="Apple"></FluentRadio>
+    <FluentRadio Value="@("banana")" Label="Banana"></FluentRadio>
+    <FluentRadio Value="@("orange")" Label="Orange"></FluentRadio>
+    <FluentRadio Value="@("grape")" Label="Grape"></FluentRadio>
+    <FluentRadio Value="@("kiwi")" Label="Kiwi"></FluentRadio>
+</FluentRadioGroup>
+
+<p>Favorite fruit: @fruit</p>
+
+@code {
+    string? fruit = "banana";
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDisabledGroup.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDisabledGroup.razor
new file mode 100644
index 0000000000..5869b56a6b
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDisabledGroup.razor
@@ -0,0 +1,12 @@
+﻿<FluentRadioGroup @bind-Value="@fruit" Disabled="true" Label="Favorite fruit">
+    <FluentRadio Value="@("apple")" Label="Apple"></FluentRadio>
+    <FluentRadio Value="@("banana")" Label="Banana"></FluentRadio>
+    <FluentRadio Value="@("orange")" Label="Orange"></FluentRadio>
+    <FluentRadio Value="@("grape")" Label="Grape"></FluentRadio>
+    <FluentRadio Value="@("kiwi")" Label="Kiwi"></FluentRadio>
+</FluentRadioGroup>
+
+
+@code {
+    private string? fruit;
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDisabledItems.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDisabledItems.razor
new file mode 100644
index 0000000000..53909de548
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupDisabledItems.razor
@@ -0,0 +1,12 @@
+﻿<FluentRadioGroup @bind-Value="@fruit" Label="Favorite fruit">
+    <FluentRadio Value="@("apple")" Label="Apple"></FluentRadio>
+    <FluentRadio Value="@("banana")" Label="Banana"></FluentRadio>
+    <FluentRadio Value="@("orange")" Label="Orange" Disabled="true"></FluentRadio>
+    <FluentRadio Value="@("grape")" Label="Grape"></FluentRadio>
+    <FluentRadio Value="@("kiwi")" Label="Kiwi" Disabled="true"></FluentRadio>
+</FluentRadioGroup>
+
+
+@code {
+    private string fruit = "banana";
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupLabelTemplate.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupLabelTemplate.razor
new file mode 100644
index 0000000000..93860608d0
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupLabelTemplate.razor
@@ -0,0 +1,24 @@
+﻿<FluentRadioGroup Name="numbers" @bind-Value="@numberValue" Label="Numbers">
+    <FluentRadio Value="1">
+        <LabelTemplate><strong>One</strong></LabelTemplate>
+    </FluentRadio>
+    <FluentRadio Value="2">
+        <LabelTemplate><em>Two</em></LabelTemplate>
+    </FluentRadio>
+</FluentRadioGroup>
+<p>
+    Selected: @(numberValue is null ? "-" : $"{numberValue} (Type: {numberValue?.GetType()})" )
+</p>
+
+<FluentRadioGroup Name="strings" @bind-Value="@stringValue" Label="Strings">
+    <FluentRadio Value="@("one")"><LabelTemplate><em>One</em></LabelTemplate></FluentRadio>
+    <FluentRadio Value="@("two")"><LabelTemplate><strong>Two</strong></LabelTemplate></FluentRadio>
+</FluentRadioGroup>
+<p>
+    Selected: @(stringValue is null ? "-" : $"{stringValue} (Type: {stringValue?.GetType()})")
+</p>
+
+@code {
+    int? numberValue;
+    string? stringValue;
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupRequired.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupRequired.razor
new file mode 100644
index 0000000000..63656ab5c8
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupRequired.razor
@@ -0,0 +1,18 @@
+﻿<form>
+    <FluentRadioGroup Name="options" Id="radio-group-required" Label="Choose an option" Required="true" @bind-Value="@value" Message="Please select a fruit." >
+        <FluentRadio Id="option1" Name="options" Value="@("Option 1")" Label="Option 1"></FluentRadio>
+        <FluentRadio Id="option2" Name="options" Value="@("Option 2")" Label="Option 2"></FluentRadio>
+        <FluentRadio Id="option3" Name="options" Value="@("Option 3")" Label="Option 3"></FluentRadio>
+    </FluentRadioGroup>
+    <br />
+    <div>
+        <FluentButton Type="ButtonType.Submit" Appearance="ButtonAppearance.Primary">Submit</FluentButton>
+        <FluentButton Id="reset-button" Type="ButtonType.Reset">Reset</FluentButton>
+    </div>
+    <span id="success-message" hidden>Form submitted successfully!</span>
+</form>
+
+
+@code {
+    private string? value;
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupVertical.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupVertical.razor
new file mode 100644
index 0000000000..87079d1b93
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupVertical.razor
@@ -0,0 +1,13 @@
+﻿@page "/radiovertical"
+<FluentRadioGroup @bind-Value="@fruit" Label="Favorite fruit" Orientation="@Orientation.Vertical">
+    <FluentRadio Label="Apple"></FluentRadio>
+    <FluentRadio Label="Banana"></FluentRadio>
+    <FluentRadio Label="Orange"></FluentRadio>
+    <FluentRadio Label="Grape"></FluentRadio>
+    <FluentRadio Label="Kiwi"></FluentRadio>
+</FluentRadioGroup>
+
+<p>Favorite fruit: @fruit</p>
+@code {
+    private string fruit = "banana";
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/FluentRadio.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/FluentRadio.md
new file mode 100644
index 0000000000..ade41ac830
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/FluentRadio.md
@@ -0,0 +1,85 @@
+---
+title: RadioGroup and Radio
+route: /Radio
+---
+
+# RadioGroup and Radio
+
+Radio groups let people select a single item from a short list. Use them in layouts that offer enough space to list up to five options or if it's important to view all options at once.
+
+If there isn't enough space, try a dropdown instead. If you need to let people select more than one option, use checkboxes. To let them immediately turn a setting on or off, try a switch.
+
+## Behavior
+
+Although it is technically possible to show a single Radio button (as can be seen below), it *must* be placed inside a RadioGroup to have any function.
+
+### Default selection
+Present a selected option in radio groups by default. The default selection should be placed first and should be the most logical response. Remaining options should be listed in a logical order. For more information, see the Content section.
+
+## Layout
+Radio groups can be aligned vertically or horizontally (default). When horizontally aligned, the label can appear next to or under the radio input.
+
+## Accessibility
+Include intuitive labels with radio groups.
+
+When tabbing, focus will fall on the first option if no options are selected. If there is a selection, focus will fall on that option first.
+
+## Content
+Keep labels short and clear
+Keep individual radio labels as concise and descriptive as possible. Use fragments instead of full sentences. If long labels can’t be avoided, text will wrap onto the next line. Never truncate radio text with an ellipsis. Use sentence case with no end punctuation.
+
+Skip the period in radio labels. For the label that introduces the radio group, don’t end with a colon. For more info, go to Periods in the Microsoft Writing Style Guide.
+
+Use sentence style capitalization—only capitalize the first word. For more info, go to Capitalization in the Microsoft Writing Style Guide.
+
+## Examples
+
+### Radio button appearances
+
+A radio button is either unchecked or checked. Usually, once an item in a group has been checked, the result of the group as a whole cannot be unchecked again.
+An item can also be disabled and can show a label to indicate the value. 
+
+{{ RadioDefault }}
+
+## RadioGroup
+
+Radios are placed and used inside a radio group. Only one of the items in a group can have a checked state.
+You can bind to the `Value` of the group to the get the value of the checked item.
+
+{{ RadioGroupDefault }}
+
+## Strongly typed items and using Label template
+Radio items allow for strongly binding to types. Because of this, string values need to be defined in the following way: 
+`Value="@("one")"`
+
+As an alternative to of using the `Label` parameter (string value only),
+it is possible to use the `LabelTemplate` parameter to specify a template for the label.
+In case both are specified, the `Label` parameter is used.
+
+{{ RadioGroupLabelTemplate }}
+
+## RadioGroup with vertical orientation
+When the radio group has a vertical orientation, the items are stacked on top of each other.
+
+{{ RadioGroupVertical }}
+
+## Disabled RadioGroup
+A radio group can be disabled as a whole. This means that the user cannot select any of the items in the group.
+{{ RadioGroupDisabledGroup }}
+
+## RadioGroup with disabled items
+Besides disabling the whole group, it is also possible to disable specific items in a group.
+{{ RadioGroupDisabledItems }}
+
+## RadioGroup required
+{{ RadioGroupRequired }}
+
+## API FluentRadioGroup
+{{ API Type=FluentRadioGroup }}
+
+## API FluentRadio
+{{ API Type=FluentRadio }}
+
+## Migrating to v5
+
+{{ INCLUDE File=MigrationFluentRadio }}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentRadio.md b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentRadio.md
new file mode 100644
index 0000000000..e86763e9f4
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentRadio.md
@@ -0,0 +1,15 @@
+---
+title: Migration FluentRadio and FluentRadioGroup
+route: /Migration/Radio
+hidden: true
+---
+
+### FluentRadio specific changes
+- The `FluentRadio` component now inherits from `FluentInputBase` (instead of `FluentComponentBase` before). This means it supports and uses all parameters from `FluentInputBase`, such as `Disabled`, `Required`, `Value`, `Label`, etc. 
+- Using the `ChildContent` parameter to specify the contents/label of a Radio item is no longer supported. Use the `Label` or `LabelTemplate` parameters instead
+- The `ReadOnly` parameter is not supported. Use the `Disabled` parameter instead.
+
+
+
+### Removed properties💥
+- `ChildContent`, use `Label` or `LabelTemplate`
diff --git a/spelling.dic b/spelling.dic
index b5fff9416b..3e88d621c8 100644
--- a/spelling.dic
+++ b/spelling.dic
@@ -51,3 +51,24 @@ tabindex
 tablist
 tabpanel
 textarea
+sourcecode
+summarydata
+currentcolor
+menuitem
+menuitems
+rightclick
+menuitemcheckbox
+menuitem
+menuitemradio
+menuitemcheckboxobsolete
+menuitemradioobsolete
+onmenuitemchange
+ontabchange
+ondropdownchange
+ondialogtoggle
+ondialogbeforetoggle
+myid
+menuclicked
+menuchecked
+rendertree
+testid
diff --git a/src/Core/Components/Radio/FluentRadio.razor b/src/Core/Components/Radio/FluentRadio.razor
new file mode 100644
index 0000000000..6456bba5ba
--- /dev/null
+++ b/src/Core/Components/Radio/FluentRadio.razor
@@ -0,0 +1,28 @@
+﻿@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@using System.Diagnostics
+@inherits FluentComponentBase
+@typeparam TValue
+@{
+    Debug.Assert(Context != null);
+}
+<FluentField ForId="@Id"
+             Class="@ClassValue"
+             Style="@StyleValue"
+             Label="@Label"
+             LabelPosition="@Components.LabelPosition.After"
+             LabelWidth="@LabelWidth"
+             IncludeInputSlot="false">
+    <LabelTemplate>@LabelTemplate</LabelTemplate>
+    <ChildContent>
+        <fluent-radio id="@Id"
+                      checked="@(Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null)"
+                      disabled="@Disabled"
+                      slot="@FluentSlot.FieldInput"
+                      value="@(Value?.ToString() ?? Label ?? null)"
+                      name="@Context.GroupName"
+                      @onchange="@Context.ChangeEventCallback"
+                      @attributes="@AdditionalAttributes">
+        </fluent-radio>
+    </ChildContent>
+</FluentField>
diff --git a/src/Core/Components/Radio/FluentRadio.razor.cs b/src/Core/Components/Radio/FluentRadio.razor.cs
new file mode 100644
index 0000000000..4362802c24
--- /dev/null
+++ b/src/Core/Components/Radio/FluentRadio.razor.cs
@@ -0,0 +1,106 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio button component.
+/// </summary>
+/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
+public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentComponentBase
+{
+    bool _trueValueToggle;
+
+    internal FluentRadioContext? Context { get; private set; }
+
+    /// <summary />
+    public FluentRadio()
+    {
+        Id = Identifier.NewId();
+    }
+
+    /// <summary>
+    /// Gets the optional CSS class. If given, this will be included in the class attribute of the component.
+    /// </summary>
+    protected virtual string? ClassValue => DefaultClassBuilder.Build();
+
+    /// <summary>
+    /// Gets the optional in-line styles. If given, these will be included in the style attribute of the component.
+    /// </summary>
+    protected virtual string? StyleValue => DefaultStyleBuilder
+        .Build();
+
+    /// <summary>
+    /// Gets or sets the name of the element.
+    /// Allows access by name from the associated form.
+    /// ⚠️ This value needs to be set manually for SSR scenarios to work correctly.
+    /// </summary>
+    [Parameter]
+    public virtual string? Name { get; set; }
+
+    /// <inheritdoc cref="IFluentField.Disabled" />
+    [Parameter]
+    public virtual bool? Disabled { get; set; }
+
+    /// <inheritdoc cref="IFluentField.Label" />
+    [Parameter]
+    public virtual string? Label { get; set; }
+
+    /// <inheritdoc cref="IFluentField.LabelTemplate" />
+    [Parameter]
+    public virtual RenderFragment? LabelTemplate { get; set; }
+
+    /// <inheritdoc cref="IFluentField.LabelWidth" />
+    [Parameter]
+    public virtual string? LabelWidth { get; set; }
+
+    /// <summary>
+    /// Gets or sets the value of the element.
+    /// </summary>
+    [Parameter]
+    public TValue? Value { get; set; }
+
+    [CascadingParameter]
+    private FluentRadioContext? CascadedContext { get; set; }
+
+    /// <inheritdoc />
+    protected override void OnParametersSet()
+    {
+        Context = string.IsNullOrEmpty(Name) ? CascadedContext : CascadedContext?.FindContextInAncestors(Name);
+
+        if (Context == null)
+        {
+            throw new InvalidOperationException($"{GetType()} must have an ancestor {typeof(FluentRadioGroup<TValue>)} " +
+                $"with a matching 'Name' property, if specified.");
+        }
+    }
+
+    // This is an unfortunate hack, but is needed for the scenario described by test InputRadioGroupWorksWithMutatingSetter.
+    // Radio groups are special in that modifying one <input type=radio> instantly and implicitly also modifies the previously
+    // selected one in the same group. As such, our SetUpdatesAttributeName mechanism isn't sufficient to stay in sync with the
+    // DOM, because the 'change' event will fire on the new <input type=radio> you just selected, not the previously-selected
+    // one, and so the previously-selected one doesn't get notified to update its state in the old rendertree. So, if the setter
+    // reverts the incoming value, the previously-selected one would produce an empty diff (because its .NET value hasn't changed)
+    // and hence it would be left unselected in the DOM. If you don't understand why this is a problem, try commenting out the
+    // line that toggles _trueValueToggle and see the E2E test fail.
+    //
+    // This hack works around that by causing InputRadio *always* to force its own 'checked' state to be true in the DOM if it's
+    // true in .NET, whether or not it was true before, by continually changing the value that represents 'true'. This doesn't
+    // really cause any significant increase in traffic because if we're rendering this InputRadio at all, sending one more small
+    // attribute value is inconsequential.
+    //
+    // Ultimately, a better solution would be to make SetUpdatesAttributeName smarter still so that it knows about the special
+    // semantics of radio buttons so that, when one <input type="radio"> changes, it treats any previously-selected sibling
+    // as needing DOM sync as well. That's a more sophisticated change and might not even be useful if the radio buttons
+    // aren't truly siblings and are in different DOM subtrees (and especially if they were rendered by different components!)
+    private string GetToggledTrueValue()
+    {
+        _trueValueToggle = !_trueValueToggle;
+        return _trueValueToggle ? "a" : "b";
+    }
+}
diff --git a/src/Core/Components/Radio/FluentRadioContext.cs b/src/Core/Components/Radio/FluentRadioContext.cs
new file mode 100644
index 0000000000..69ab34336e
--- /dev/null
+++ b/src/Core/Components/Radio/FluentRadioContext.cs
@@ -0,0 +1,49 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.Components;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// Describes context for an <see cref="FluentRadio{TValue}"/> component.
+/// </summary>
+internal sealed class FluentRadioContext
+{
+    private readonly IFluentRadioValueProvider _valueProvider;
+
+    public FluentRadioContext? ParentContext { get; }
+    public EventCallback<ChangeEventArgs> ChangeEventCallback { get; }
+    public object? CurrentValue => _valueProvider.CurrentValue;
+
+    // Mutable properties that may change any time an FluentRadioGroup is rendered
+    public string? GroupName { get; set; }
+
+    public string? FieldClass { get; set; }
+
+    public Orientation? Orientation { get; set; }
+
+    /// <summary>
+    /// Instantiates a new <see cref="FluentRadioContext" />.
+    /// </summary>
+    /// <param name="parentContext">The parent context, if any.</param>
+    /// <param name="changeEventCallback">The event callback to be invoked when the selected value is changed.</param>
+    /// <param name="valueProvider">The value provider for the radio context.</param>
+    /// <param name="orientation">The orientation of the radio group.</param>
+    public FluentRadioContext(IFluentRadioValueProvider valueProvider, FluentRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback, Orientation? orientation)
+    {
+        _valueProvider = valueProvider;
+        ParentContext = parentContext;
+        ChangeEventCallback = changeEventCallback;
+        Orientation = orientation;
+    }
+
+    /// <summary>
+    /// Finds an <see cref="FluentRadioContext"/> in the context's ancestors with the matching <paramref name="groupName"/>.
+    /// </summary>
+    /// <param name="groupName">The group name of the ancestor <see cref="FluentRadioContext"/>.</param>
+    /// <returns>The <see cref="FluentRadioContext"/>, or <c>null</c> if none was found.</returns>
+    public FluentRadioContext? FindContextInAncestors(string groupName)
+        => string.Equals(GroupName, groupName, StringComparison.InvariantCulture) ? this : ParentContext?.FindContextInAncestors(groupName);
+}
diff --git a/src/Core/Components/Radio/FluentRadioGroup.razor b/src/Core/Components/Radio/FluentRadioGroup.razor
new file mode 100644
index 0000000000..578243b272
--- /dev/null
+++ b/src/Core/Components/Radio/FluentRadioGroup.razor
@@ -0,0 +1,23 @@
+﻿@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@inherits FluentInputBase<TValue>
+@typeparam TValue
+<CascadingValue TValue="FluentRadioContext" Value="Context">
+    <FluentField InputComponent="@this" ForId="@Id" Class="@ClassValue" Style="@StyleValue" >
+        <fluent-radio-group id="@Id"
+                            disabled="@Disabled"
+                            slot="@FluentSlot.FieldInput"
+                            aria-labelledby="radio-group-default--label"
+                            name="@Name"
+                            value="@Context?.CurrentValue"
+                            orientation="@Orientation.ToAttributeValue()"
+                            required="@Required"
+                            readonly="@ReadOnly"
+                            aria-label="@AriaLabel"
+                            autofocus="@Autofocus"
+                            @onfocusout="@FocusOutHandlerAsync"
+                            @attributes="@AdditionalAttributes">
+            @ChildContent
+        </fluent-radio-group>
+    </FluentField>
+</CascadingValue>
diff --git a/src/Core/Components/Radio/FluentRadioGroup.razor.cs b/src/Core/Components/Radio/FluentRadioGroup.razor.cs
new file mode 100644
index 0000000000..61ccd310d6
--- /dev/null
+++ b/src/Core/Components/Radio/FluentRadioGroup.razor.cs
@@ -0,0 +1,85 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Forms;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio Group component.
+/// Groups <see cref="FluentRadio{TValue}"/> components together.
+/// </summary>
+/// <typeparam name="TValue">The type of the value</typeparam>
+[CascadingTypeParameter(nameof(TValue))]
+public partial class FluentRadioGroup<TValue> : FluentInputBase<TValue>, IFluentRadioValueProvider
+{
+    private readonly string _defaultGroupName = Identifier.NewId();
+    internal FluentRadioContext? Context { get; private set; }
+
+    object? IFluentRadioValueProvider.CurrentValue => CurrentValue;
+
+    /// <summary />
+    public FluentRadioGroup()
+    {
+        Id = Identifier.NewId();
+    }
+
+    /// <summary>
+    /// Gets or sets the orientation of the group. See <see cref="AspNetCore.Components.Orientation"/>
+    /// </summary>
+    [Parameter]
+    public Orientation? Orientation { get; set; }
+
+    /// <summary>
+    /// Gets or sets the child content to be rendering inside the <see cref="FluentRadioGroup{TValue}"/>.
+    /// </summary>
+    [Parameter]
+    public RenderFragment? ChildContent { get; set; }
+
+    [CascadingParameter]
+    private FluentRadioContext? CascadedContext { get; set; }
+
+    /// <inheritdoc />
+    [ExcludeFromCodeCoverage(Justification = "As part of the code cannot be executed in any known usage pattern, it can't be tested.")]
+    protected override void OnParametersSet()
+    {
+        // On the first render, we can instantiate the FluentRadioContext
+        if (Context is null)
+        {
+            var changeEventCallback = EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString);
+            Context = new FluentRadioContext(this, CascadedContext, changeEventCallback, Orientation);
+        }
+        else if (Context.ParentContext != CascadedContext)
+        {
+            // This should never be possible in any known usage pattern, but if it happens, we want to know
+            throw new InvalidOperationException("A FluentRadioGroup cannot change context after creation");
+        }
+
+        // Mutate the FluentRadioContext instance in place. Since this is a non-fixed cascading parameter, the descendant
+        // FluentRadio/FluentRadioGroup components will get notified to re-render and will see the new values.
+        Context.GroupName = !string.IsNullOrEmpty(Name) ? Name : _defaultGroupName;
+
+        Context.FieldClass = EditContext?.FieldCssClass(FieldIdentifier);
+    }
+
+    /// <inheritdoc />
+    protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
+    => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
+
+    /// <summary>
+    /// Handler for the OnFocus event.
+    /// </summary>
+    /// <param name="e"></param>
+    /// <returns></returns>
+    protected virtual Task FocusOutHandlerAsync(FocusEventArgs e)
+    {
+        FocusLost = true;
+        return Task.CompletedTask;
+    }
+}
diff --git a/src/Core/Components/Radio/IFluentRadioValueProvider.cs b/src/Core/Components/Radio/IFluentRadioValueProvider.cs
new file mode 100644
index 0000000000..d8de9c08c7
--- /dev/null
+++ b/src/Core/Components/Radio/IFluentRadioValueProvider.cs
@@ -0,0 +1,10 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+internal interface IFluentRadioValueProvider
+{
+    public object? CurrentValue { get; }
+}
diff --git a/src/Core/Extensions/FluentInputExtensions.cs b/src/Core/Extensions/FluentInputExtensions.cs
index 3f27b1eac1..eff6f7e2af 100644
--- a/src/Core/Extensions/FluentInputExtensions.cs
+++ b/src/Core/Extensions/FluentInputExtensions.cs
@@ -2,9 +2,9 @@
 // MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
 // ------------------------------------------------------------------------
 
-using Microsoft.AspNetCore.Components;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using Microsoft.AspNetCore.Components;
 
 namespace Microsoft.FluentUI.AspNetCore.Components.Extensions;
 
diff --git a/tests/Core/Components/Base/ComponentBaseTests.cs b/tests/Core/Components/Base/ComponentBaseTests.cs
index f145d39be6..3c641232ea 100644
--- a/tests/Core/Components/Base/ComponentBaseTests.cs
+++ b/tests/Core/Components/Base/ComponentBaseTests.cs
@@ -5,11 +5,9 @@
 using System.Reflection;
 using System.Text;
 using System.Text.RegularExpressions;
-using System.Xml;
 using Bunit;
 using Microsoft.AspNetCore.Components;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.FluentUI.AspNetCore.Components;
 using Microsoft.JSInterop;
 using Xunit;
 
@@ -24,6 +22,7 @@ public class ComponentBaseTests : Bunit.TestContext
     {
         typeof(AspNetCore.Components._Imports),
         typeof(DialogOptions),
+        typeof(FluentRadio<>),
     };
 
     /// <summary>
@@ -35,6 +34,8 @@ public class ComponentBaseTests : Bunit.TestContext
         { typeof(FluentSelect<>), Loader.MakeGenericType(typeof(int))},
         { typeof(FluentCombobox<>), Loader.MakeGenericType(typeof(int))},
         { typeof(FluentSlider<>), Loader.MakeGenericType(typeof(int))},
+        { typeof(FluentRadioGroup<>), Loader.MakeGenericType(typeof(string)) },
+        //{ typeof(FluentRadio<>), Loader.MakeGenericType(typeof(string)) },
         { typeof(FluentTooltip), Loader.Default.WithRequiredParameter("Anchor", "MyButton").WithRequiredParameter("UseTooltipService", false)},
     };
 
@@ -143,7 +144,7 @@ public void ComponentBase_TooltipInterface_CorrectRendering()
             });
 
             // Assert
-            
+
             var isMatch = Regex.IsMatch(renderedComponent.Markup, $"<fluent-tooltip .+><text>My tooltip {type.Name}<\\/text><\\/fluent-tooltip>");
 
             Output.WriteLine($"{(isMatch ? "✅" : "❌")} {componentType.Name}");
diff --git a/tests/Core/Components/Base/InputBaseTests.cs b/tests/Core/Components/Base/InputBaseTests.cs
index 677173b4bf..99b341de77 100644
--- a/tests/Core/Components/Base/InputBaseTests.cs
+++ b/tests/Core/Components/Base/InputBaseTests.cs
@@ -7,16 +7,10 @@
  * ********************************************************
  */
 
-using System.Reflection;
 using System.Text;
-using System.Text.RegularExpressions;
 using Bunit;
 using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Web;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.FluentUI.AspNetCore.Components;
-using Microsoft.JSInterop;
-using Microsoft.VisualStudio.TestPlatform.Utilities;
 using Xunit;
 
 namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base;
@@ -28,7 +22,8 @@ public class InputBaseTests : Bunit.TestContext
     /// </summary>
     private static readonly Type[] Excluded = new[]
     {
-        typeof(AspNetCore.Components._Imports)
+        typeof(AspNetCore.Components._Imports),
+        typeof(FluentRadio<>),
     };
 
     /// <summary>
@@ -40,6 +35,8 @@ public class InputBaseTests : Bunit.TestContext
         { typeof(FluentSelect<>), type => type.MakeGenericType(typeof(string)) },
         { typeof(FluentCombobox<>), type => type.MakeGenericType(typeof(string)) },
         { typeof(FluentSlider<>), type => type.MakeGenericType(typeof(int)) },
+        { typeof(FluentRadioGroup<>), type => type.MakeGenericType(typeof(string)) },
+        //{ typeof(FluentRadio<>), type => type.MakeGenericType(typeof(string)) },
     };
 
     /// <summary />
diff --git a/tests/Core/Components/Radio/FluentRadioGroupTests.FluentRadioGroup_Default.verified.razor.html b/tests/Core/Components/Radio/FluentRadioGroupTests.FluentRadioGroup_Default.verified.razor.html
new file mode 100644
index 0000000000..4d9a3a7d88
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioGroupTests.FluentRadioGroup_Default.verified.razor.html
@@ -0,0 +1,5 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label"></fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioGroupTests.FluentRadioGroup_WithChildContent.verified.razor.html b/tests/Core/Components/Radio/FluentRadioGroupTests.FluentRadioGroup_WithChildContent.verified.razor.html
new file mode 100644
index 0000000000..118a53cf14
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioGroupTests.FluentRadioGroup_WithChildContent.verified.razor.html
@@ -0,0 +1,24 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" value="" blazor:onfocusout="1">
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">Option 1
+      </label>
+      <fluent-radio id="xxx" slot="input" value="option1" name="xxx" blazor:onchange="2"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">Option 2
+      </label>
+      <fluent-radio id="xxx" slot="input" value="option2" name="xxx" blazor:onchange="3"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">Option 3
+      </label>
+      <fluent-radio id="xxx" slot="input" value="option3" name="xxx" blazor:onchange="4"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioGroupTests.razor b/tests/Core/Components/Radio/FluentRadioGroupTests.razor
new file mode 100644
index 0000000000..bba0f2e813
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioGroupTests.razor
@@ -0,0 +1,292 @@
+﻿@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@using Microsoft.FluentUI.AspNetCore.Components.Tests.Extensions
+@using Microsoft.FluentUI.AspNetCore.Components.Utilities
+@using System.ComponentModel.DataAnnotations
+@using Xunit;
+@inherits Bunit.TestContext
+@implements IFluentRadioValueProvider
+@code {
+
+    object IFluentRadioValueProvider.CurrentValue => new();
+
+    public FluentRadioGroupTests()
+    {
+        JSInterop.Mode = JSRuntimeMode.Loose;
+        Services.AddFluentUIComponents();
+    }
+
+    [Fact]
+    public void FluentRadioGroup_Default()
+    {
+        // Arrange & Act
+        var cut = Render(@<FluentRadioGroup TValue="string" />);
+
+        // Assert
+        cut.Verify();
+    }
+
+    [Fact]
+    public void FluentRadioGroup_WithChildContent()
+    {
+        // Arrange & Act
+        var value = "";
+        var cut = Render(
+            @<FluentRadioGroup @bind-Value="@value">
+                <FluentRadio Value="@("option1")" Label="Option 1"></FluentRadio>
+                <FluentRadio Value="@("option2")" Label="Option 2"></FluentRadio>
+                <FluentRadio Value="@("option3")" Label="Option 3"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        cut.Verify();
+    }
+
+    [Fact]
+    public void FluentRadioGroup_StringBinding()
+    {
+        // Arrange
+        string selectedValue = "option2";
+
+        // Act
+        var cut = Render(
+            @<FluentRadioGroup Value="@selectedValue">
+                <FluentRadio Value="@("option1")" Label="Option 1"></FluentRadio>
+                <FluentRadio Value="@("option2")" Label="Option 2"></FluentRadio>
+                <FluentRadio Value="@("option3")" Label="Option 3"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radioGroup = cut.Find("fluent-radio-group");
+        Assert.Equal(selectedValue, radioGroup.GetAttribute("value"));
+    }
+
+    [Fact]
+    public void FluentRadioGroup_IntBinding()
+    {
+        // Arrange
+        int selectedValue = 2;
+
+        // Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="int" Value="@selectedValue">
+                <FluentRadio TValue="int" Value="1">Option 1</FluentRadio>
+                <FluentRadio TValue="int" Value="2">Option 2</FluentRadio>
+                <FluentRadio TValue="int" Value="3">Option 3</FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radioGroup = cut.Find("fluent-radio-group");
+        Assert.Equal(selectedValue.ToString(), radioGroup.GetAttribute("value"));
+    }
+
+    [Fact]
+    public void FluentRadioGroup_EnumBinding()
+    {
+        // Arrange
+        TestEnum selectedValue = TestEnum.Option2;
+
+        // Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="TestEnum" Value="@selectedValue">
+                <FluentRadio Value="TestEnum.Option1" Label="Option 1"></FluentRadio>
+                <FluentRadio Value="TestEnum.Option2" Label="Option 2"></FluentRadio>
+                <FluentRadio Value="TestEnum.Option3" Label="Option 3"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radioGroup = cut.Find("fluent-radio-group");
+        Assert.Equal(selectedValue.ToString(), radioGroup.GetAttribute("value"));
+    }
+
+    [Theory]
+    [InlineData(Orientation.Horizontal)]
+    [InlineData(Orientation.Vertical)]
+    [InlineData(null)]
+    public void FluentRadioGroup_Orientation(Orientation? orientation)
+    {
+        // Arrange & Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="string" Orientation="@orientation">
+                <FluentRadio Label="option1"></FluentRadio>
+                <FluentRadio Label="option2"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radioGroup = cut.Find("fluent-radio-group");
+        var orientAttr = radioGroup.GetAttribute("orientation");
+
+        if (orientation == null)
+        {
+            Assert.Null(orientAttr);
+        }
+        else
+        {
+            Assert.Equal(orientation?.ToString().ToLower(), orientAttr);
+        }
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public void FluentRadioGroup_Required(bool isRequired)
+    {
+        // Arrange & Act
+        var cut = Render(@<FluentRadioGroup TValue="string" Required="@isRequired" />);
+
+        // Assert
+        var radioGroup = cut.Find("fluent-radio-group");
+        if (isRequired)
+        {
+            Assert.NotNull(radioGroup.GetAttribute("required"));
+        }
+        else
+        {
+            Assert.Null(radioGroup.GetAttribute("required"));
+        }
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public void FluentRadioGroup_Disabled(bool isDisabled)
+    {
+        // Arrange & Act
+        var cut = Render(@<FluentRadioGroup TValue="string" Disabled="@isDisabled" />);
+
+        // Assert
+        var radioGroup = cut.Find("fluent-radio-group");
+        if (isDisabled)
+        {
+            Assert.NotNull(radioGroup.GetAttribute("disabled"));
+        }
+        else
+        {
+            Assert.Null(radioGroup.GetAttribute("disabled"));
+        }
+    }
+
+    [Fact]
+    public void FluentRadioGroup_Name()
+    {
+        // Arrange & Act
+        var name = "test-radio-group";
+        var cut = Render(@<FluentRadioGroup TValue="string" Name="@name" />);
+
+        // Assert
+        var radioGroup = cut.Find("fluent-radio-group");
+        Assert.Equal(name, radioGroup.GetAttribute("name"));
+    }
+
+    [Fact]
+    public void FluentRadioGroup_ClassValue()
+    {
+        // Arrange & Act
+        var className = "custom-class";
+        var cut = Render(@<FluentRadioGroup TValue="string" Class="@className" />);
+
+        // Assert
+        var radioGroup = cut.Find("fluent-field");
+        Assert.Contains(className, radioGroup.GetAttribute("class"));
+    }
+
+    [Fact]
+    public void FluentRadioGroup_ValueChanged()
+    {
+        // Arrange
+        string? selectedValue = null;
+        var newValue = "option2";
+
+        var cut = Render(
+            @<FluentRadioGroup TValue="string" @bind-Value="selectedValue">
+                <FluentRadio Label="option1"></FluentRadio>
+                <FluentRadio Label="option2"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Act
+        cut.Find("fluent-radio[value='option2']").Change(newValue);
+
+        // Assert
+        Assert.Equal(newValue, selectedValue);
+    }
+
+    [Fact]
+    public void FluentRadioGroup_DefaultGroupName()
+    {
+        // Arrange & Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="string">
+                <FluentRadio TValue="string" Label="option1"></FluentRadio>
+            </FluentRadioGroup>
+    );
+
+        // Assert
+        var radio = cut.Find("fluent-radio");
+        Assert.NotNull(radio.GetAttribute("name"));
+        // The name should be a generated ID
+        Assert.NotEqual(string.Empty, radio.GetAttribute("name"));
+    }
+
+    [Fact]
+    public void FluentRadioGroup_RadioChecked()
+    {
+        // Arrange & Act
+        var cut = Render(@<FluentRadioGroup TValue="string" Value="@("option1")">
+                <FluentRadio TValue="string" Value="@("option1")"></FluentRadio>
+            </FluentRadioGroup>);
+
+        // Assert
+        var attribute = cut.Find("fluent-radio").GetAttribute("checked") ?? null;
+        Assert.NotNull(attribute);
+    }
+
+    [Fact]
+    public void FluentRadioGroup_FindContext()
+    {
+        // Arrange
+        FluentRadioGroup<string> group = new FluentRadioGroup<string>();
+        var args = new ChangeEventArgs
+        {
+            Value = "option1"
+        };
+
+        var callback = EventCallback.Factory.Create<ChangeEventArgs>(this, HandleCallBack);
+
+        FluentRadioContext? fluentRadioContextParent = new FluentRadioContext(this, null, callback, null);
+        fluentRadioContextParent.GroupName = "test-group";
+        FluentRadioContext fluentRadioContext = new FluentRadioContext(this, fluentRadioContextParent, callback, null);
+        var cut = Render(@<CascadingValue TValue = "FluentRadioContext" Value="@fluentRadioContext">
+            <FluentRadioGroup @ref="@group" TValue="string">
+                <FluentRadio TValue="string" Label="option1"></FluentRadio>
+            </FluentRadioGroup>
+            </CascadingValue>
+        );
+
+        // Act
+        var context = group.Context;
+
+        // Assert
+        Assert.NotNull(context);
+        Assert.Equal(fluentRadioContextParent, context.FindContextInAncestors("test-group"));
+
+
+    }
+
+    private void HandleCallBack(ChangeEventArgs args)
+    {
+        // Handle the callback
+    }
+
+    public enum TestEnum
+    {
+        Option1,
+        Option2,
+        Option3
+    }
+
+}
diff --git a/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_Default.verified.razor.html b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_Default.verified.razor.html
new file mode 100644
index 0000000000..d5f14e3042
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_Default.verified.razor.html
@@ -0,0 +1,12 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" blazor:onfocusout="1">
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">
+      </label>
+      <fluent-radio id="xxx" slot="input" name="xxx" blazor:onchange="2"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_HorizontalOrientation.verified.razor.html b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_HorizontalOrientation.verified.razor.html
new file mode 100644
index 0000000000..dce35af81d
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_HorizontalOrientation.verified.razor.html
@@ -0,0 +1,18 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" value="0" orientation="horizontal" blazor:onfocusout="1">
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">Option 1
+      </label>
+      <fluent-radio id="xxx" slot="input" value="1" name="xxx" blazor:onchange="2"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">Option 2
+      </label>
+      <fluent-radio id="xxx" slot="input" value="2" name="xxx" blazor:onchange="3"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_Label.verified.razor.html b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_Label.verified.razor.html
new file mode 100644
index 0000000000..15d376ffc9
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_Label.verified.razor.html
@@ -0,0 +1,12 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" blazor:onfocusout="1">
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">Test Label
+      </label>
+      <fluent-radio id="xxx" slot="input" value="Test Label" name="xxx" blazor:onchange="2"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelPosition-after.verified.razor.html b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelPosition-after.verified.razor.html
new file mode 100644
index 0000000000..56a6b226bd
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelPosition-after.verified.razor.html
@@ -0,0 +1,12 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" blazor:onfocusout="1">
+    <fluent-field id="xxx" label-position="after" class="my-3-o">
+      <label id="xxx" slot="label" for="xxx">Test Label
+      </label>
+      <fluent-radio id="xxx" slot="input" value="Test Label" name="xxx" blazor:onfocusout="2" blazor:onchange="3"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelPosition-before.verified.razor.html b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelPosition-before.verified.razor.html
new file mode 100644
index 0000000000..56a6b226bd
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelPosition-before.verified.razor.html
@@ -0,0 +1,12 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" blazor:onfocusout="1">
+    <fluent-field id="xxx" label-position="after" class="my-3-o">
+      <label id="xxx" slot="label" for="xxx">Test Label
+      </label>
+      <fluent-radio id="xxx" slot="input" value="Test Label" name="xxx" blazor:onfocusout="2" blazor:onchange="3"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelTemplate.verified.razor.html b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelTemplate.verified.razor.html
new file mode 100644
index 0000000000..f0dd64b4e9
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelTemplate.verified.razor.html
@@ -0,0 +1,13 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" blazor:onfocusout="1">
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx">
+        <div>Custom Label Template</div>
+      </label>
+      <fluent-radio id="xxx" slot="input" name="xxx" blazor:onchange="2"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelWidth.verified.razor.html b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelWidth.verified.razor.html
new file mode 100644
index 0000000000..ea94617d70
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.FluentRadio_LabelWidth.verified.razor.html
@@ -0,0 +1,12 @@
+
+<fluent-field id="xxx" label-position="above">
+  <fluent-radio-group id="xxx" slot="input" aria-labelledby="radio-group-default--label" blazor:onfocusout="1">
+    <fluent-field label-position="after" class="my-3-o">
+      <label slot="label" for="xxx" style="width: 200px;">Test Label
+      </label>
+      <fluent-radio id="xxx" slot="input" value="Test Label" name="xxx" blazor:onchange="2"></fluent-radio>
+      <fluent-text as="span" size="200" slot="message"></fluent-text>
+    </fluent-field>
+  </fluent-radio-group>
+  <fluent-text as="span" size="200" slot="message"></fluent-text>
+</fluent-field>
\ No newline at end of file
diff --git a/tests/Core/Components/Radio/FluentRadioTests.razor b/tests/Core/Components/Radio/FluentRadioTests.razor
new file mode 100644
index 0000000000..74a01dd718
--- /dev/null
+++ b/tests/Core/Components/Radio/FluentRadioTests.razor
@@ -0,0 +1,205 @@
+﻿@using Bunit.Rendering
+@using Microsoft.FluentUI.AspNetCore.Components.Tests.Extensions
+@using Microsoft.FluentUI.AspNetCore.Components.Utilities
+@using Microsoft.AspNetCore.Components.Forms
+@using Xunit
+
+@using System.Linq.Expressions;
+@using Microsoft.AspNetCore.Components.Rendering;
+@using Microsoft.AspNetCore.Components.RenderTree;
+
+
+@inherits Bunit.TestContext
+
+@code {
+    public FluentRadioTests()
+    {
+        JSInterop.Mode = JSRuntimeMode.Loose;
+        Services.AddFluentUIComponents();
+    }
+
+    [Fact]
+    public void FluentRadio_Default()
+    {
+        // Arrange & Act
+        var radioGroup = new FluentRadioGroup<string>();
+        var cut = Render(
+            @<FluentRadioGroup TValue="string" @ref="radioGroup">
+                <FluentRadio TValue="string"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        cut.Verify();
+    }
+
+    [Fact]
+    public void FluentRadio_Value()
+    {
+        // Arrange & Act
+        var radioGroup = new FluentRadioGroup<string>();
+        var value = "test-value";
+        var cut = Render(
+            @<FluentRadioGroup TValue="string" @ref="radioGroup">
+                <FluentRadio TValue="string" Value="@value"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radio = cut.Find("fluent-radio");
+        Assert.Equal(value, radio.GetAttribute("value"));
+    }
+
+    [Fact]
+    public void FluentRadio_Checked()
+    {
+        // Arrange & Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="string" Value="@("test-value")">
+                <FluentRadio TValue="string" Value="@("test-value")"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radio = cut.Find("fluent-radio");
+        var result = radio.GetAttribute("checked");
+        Assert.NotNull(result);
+        Assert.Equal("a", result); // Added assertion to check if the radio is checked
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public void FluentRadio_Disabled(bool isDisabled)
+    {
+        // Arrange & Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="string">
+                <FluentRadio TValue="string" Disabled="@isDisabled"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radio = cut.Find("fluent-radio");
+        if (isDisabled)
+        {
+            Assert.NotNull(radio.GetAttribute("disabled"));
+        }
+        else
+        {
+            Assert.Null(radio.GetAttribute("disabled"));
+        }
+    }
+
+    [Fact]
+    public void FluentRadio_Label()
+    {
+        // Arrange & Act
+        var label = "Test Label";
+        var cut = Render(
+            @<FluentRadioGroup TValue="string">
+                <FluentRadio TValue="string" Label="@label"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        cut.Verify();
+    }
+
+    [Fact]
+    public void FluentRadio_LabelWidth()
+    {
+        // Arrange & Act
+        var labelWidth = "200px";
+        var cut = Render(
+            @<FluentRadioGroup TValue="string">
+                <FluentRadio TValue="string" Label="Test Label" LabelWidth="@labelWidth"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        cut.Verify();
+    }
+
+    [Fact]
+    public void FluentRadio_LabelTemplate()
+    {
+        // Arrange & Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="string">
+                <FluentRadio TValue="string">
+                    <LabelTemplate>
+                        <div>Custom Label Template</div>
+                    </LabelTemplate>
+                </FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        cut.Verify();
+    }
+
+    [Fact]
+    public void FluentRadio_Name()
+    {
+        // Arrange & Act
+        var name = "test-radio-group";
+        var cut = Render(
+            @<FluentRadioGroup TValue="string" Name="@name">
+                <FluentRadio TValue="string"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radio = cut.Find("fluent-radio");
+        Assert.Equal(name, radio.GetAttribute("name"));
+    }
+
+    [Fact]
+    public void FluentRadio_ThrowsIfNotInRadioGroup()
+    {
+        // This test verifies that a FluentRadio component throws if not inside a FluentRadioGroup
+
+        // Arrange & Act & Assert
+        Assert.Throws<InvalidOperationException>(() =>
+        {
+            Render(@<FluentRadio TValue="string" Value="@("test")"></FluentRadio>);
+        });
+    }
+
+    [Fact]
+    public void FluentRadio_AdditionalAttributes()
+    {
+        // Arrange & Act
+        var attributes = new Dictionary<string, object>
+        {
+            { "data-testid", "test-radio" },
+            { "class", "custom-class" }
+        };
+
+        var cut = Render(
+            @<FluentRadioGroup TValue="string">
+                <FluentRadio TValue="string" @attributes="attributes"></FluentRadio>
+            </FluentRadioGroup>
+        );
+
+        // Assert
+        var radio = cut.Find("fluent-radio");
+        Assert.Equal("test-radio", radio.GetAttribute("data-testid"));
+    }
+
+    [Fact]
+    public void FluentRadio_HorizontalOrientation()
+    {
+        // Arrange & Act
+        var cut = Render(
+            @<FluentRadioGroup TValue="int" Orientation="Orientation.Horizontal">
+                <FluentRadio Value="1" Label="Option 1"></FluentRadio>
+                <FluentRadio Value="2" Label="Option 2"></FluentRadio>
+            </FluentRadioGroup>
+    );
+
+        // Assert
+        cut.Verify();
+    }
+}


Reviews:
user: login: dvoituron
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T14:04:48Z
user: login: dvoituron
body: Can you add a PR description and update the status to "Ready for review"?

state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T17:39:00Z
user: login: dvoituron
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T18:06:06Z
user: login: vnbaaij
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T18:52:10Z
user: login: vnbaaij
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T18:56:16Z
user: login: dvoituron
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T20:18:39Z
user: login: dvoituron
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-14T07:28:57Z
user: login: dvoituron
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T08:50:16Z
user: login: dvoituron
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T18:53:23Z
user: login: vnbaaij
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T19:06:45Z
user: login: vnbaaij
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T19:10:50Z
user: login: dvoituron
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-15T06:52:59Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+﻿<FluentRadioGroup Name="numbers" @bind-Value=value1 Label="Numbers">
path: examples/Demo/FluentUI.Demo.Client/Documentation/Components/Radio/Examples/RadioGroupLabelTemplate.razor
user: login: dvoituron
body: Can you use the syntax `@bind-Value="@value1"`. And the same for `value2`. 
created_at: 2025-04-13T13:58:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,81 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio button component.
+/// </summary>
+/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
+public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>, IFluentField
+{
+    bool _trueValueToggle;
+
+    internal FluentRadioContext? Context { get; private set; }
+
+    /// <summary />
+    public FluentRadio()
+    {
+        Id = Identifier.NewId();
+    }
+
+    [CascadingParameter]
+    private FluentRadioContext? CascadedContext { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the element is checked.
+    /// </summary>
+    [Parameter]
+    public bool? Checked { get; set; }
path: src/Core/Components/Radio/FluentRadio.razor.cs
user: login: dvoituron
body: I don't understand in which conditions this parameter will be used (outside of a Group). And in this case, we should create a bindable property. But for me, we could remove it and "force" the dev to use an Radio only inside a group. 
created_at: 2025-04-13T18:04:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,81 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio button component.
+/// </summary>
+/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
+public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>, IFluentField
+{
+    bool _trueValueToggle;
+
+    internal FluentRadioContext? Context { get; private set; }
+
+    /// <summary />
+    public FluentRadio()
+    {
+        Id = Identifier.NewId();
path: src/Core/Components/Radio/FluentRadio.razor.cs
user: login: dvoituron
body: Why do we force to have an ID? Not working without that?
created_at: 2025-04-13T18:06:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,81 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio button component.
+/// </summary>
+/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
+public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>, IFluentField
+{
+    bool _trueValueToggle;
+
+    internal FluentRadioContext? Context { get; private set; }
+
+    /// <summary />
+    public FluentRadio()
+    {
+        Id = Identifier.NewId();
path: src/Core/Components/Radio/FluentRadio.razor.cs
user: login: vnbaaij
body: I assumed it was needed for the FluentField. But it isn't, so I've removed the constructor code.
created_at: 2025-04-13T18:52:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,81 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio button component.
+/// </summary>
+/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
+public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>, IFluentField
+{
+    bool _trueValueToggle;
+
+    internal FluentRadioContext? Context { get; private set; }
+
+    /// <summary />
+    public FluentRadio()
+    {
+        Id = Identifier.NewId();
+    }
+
+    [CascadingParameter]
+    private FluentRadioContext? CascadedContext { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the element is checked.
+    /// </summary>
+    [Parameter]
+    public bool? Checked { get; set; }
path: src/Core/Components/Radio/FluentRadio.razor.cs
user: login: vnbaaij
body: It is used to set the initial checked state of a radio item in a group. We do not want to make this bindable as the binding takes place on the group level (with the `Value` parameter)
created_at: 2025-04-13T18:56:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,81 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio button component.
+/// </summary>
+/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
+public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>, IFluentField
+{
+    bool _trueValueToggle;
+
+    internal FluentRadioContext? Context { get; private set; }
+
+    /// <summary />
+    public FluentRadio()
+    {
+        Id = Identifier.NewId();
+    }
+
+    [CascadingParameter]
+    private FluentRadioContext? CascadedContext { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the element is checked.
+    /// </summary>
+    [Parameter]
+    public bool? Checked { get; set; }
path: src/Core/Components/Radio/FluentRadio.razor.cs
user: login: dvoituron
body: Weird. And what if the first option `FluentOption.Checked=true` and the `FluentOptionGroup.Value="Second"` ?
Why not to use only the bindable FluentOptionGroupd value?
created_at: 2025-04-13T20:18:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+//using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+/// <summary>
+/// A Fluent Radio button component.
+/// </summary>
+/// <typeparam name="TValue">The type for the value of the radio button</typeparam>
+public partial class FluentRadio<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase<TValue>, IFluentField
path: src/Core/Components/Radio/FluentRadio.razor.cs
user: login: dvoituron
body: I don't think you need to implement `IFluentField`. Because the dev could use all properties like `LabelPosition`, `Message`, `MessageIcon` ... and without effect :-(
created_at: 2025-04-14T08:50:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+﻿@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@using System.Diagnostics
+@inherits FluentComponentBase
+@typeparam TValue
+@{
+    Debug.Assert(Context != null);
+}
+<FluentField ForId="@Id"
+             Class="@ClassValue"
+             Style="@StyleValue"
+             Label="@Label"
+             LabelPosition="@Components.LabelPosition.After"
+             LabelWidth="@LabelWidth"
+             IncludeInputSlot="false">
+    <LabelTemplate>@LabelTemplate</LabelTemplate>
+    <ChildContent>
+        <fluent-radio id="@Id"
+                      checked="@(Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null)"
path: src/Core/Components/Radio/FluentRadio.razor
user: login: dvoituron
body: As I understand the web component, the `RadioGroup.Value` is used to check the right Radio. This property would only be used if a `FluentRadio` was used outside a `Group`, which is not possible with us.
created_at: 2025-04-14T18:48:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+﻿@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@using System.Diagnostics
+@inherits FluentComponentBase
+@typeparam TValue
+@{
+    Debug.Assert(Context != null);
+}
+<FluentField ForId="@Id"
+             Class="@ClassValue"
+             Style="@StyleValue"
+             Label="@Label"
+             LabelPosition="@Components.LabelPosition.After"
+             LabelWidth="@LabelWidth"
+             IncludeInputSlot="false">
+    <LabelTemplate>@LabelTemplate</LabelTemplate>
+    <ChildContent>
+        <fluent-radio id="@Id"
+                      checked="@(Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null)"
+                      disabled="@Disabled"
+                      slot="@FluentSlot.FieldInput"
+                      value="@(Value?.ToString() ?? Label ?? null)"
+                      name="@Context.GroupName"
+                      @onchange="@Context.ChangeEventCallback"
+                      @attributes="AdditionalAttributes">
path: src/Core/Components/Radio/FluentRadio.razor
user: login: dvoituron
body: Can you add the `@` to be clearer? `@AdditionalAttributes`
created_at: 2025-04-14T18:49:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+﻿@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@using System.Diagnostics
+@inherits FluentComponentBase
+@typeparam TValue
+@{
+    Debug.Assert(Context != null);
+}
+<FluentField ForId="@Id"
+             Class="@ClassValue"
+             Style="@StyleValue"
+             Label="@Label"
+             LabelPosition="@Components.LabelPosition.After"
+             LabelWidth="@LabelWidth"
+             IncludeInputSlot="false">
+    <LabelTemplate>@LabelTemplate</LabelTemplate>
+    <ChildContent>
+        <fluent-radio id="@Id"
+                      checked="@(Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null)"
+                      disabled="@Disabled"
+                      slot="@FluentSlot.FieldInput"
+                      value="@(Value?.ToString() ?? Label ?? null)"
+                      name="@Context.GroupName"
+                      @onchange="@Context.ChangeEventCallback"
path: src/Core/Components/Radio/FluentRadio.razor
user: login: dvoituron
body: You can catch the value changed from the `FluentRadioGroup`
created_at: 2025-04-14T18:50:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+internal interface IFluentRadioValueProvider
path: src/Core/Components/Base/IFluentRadioValueProvider.cs
user: login: dvoituron
body: This interface is only used with the FluentRadio elements. Probably better to move this file in the `Radio` folder
created_at: 2025-04-14T18:51:33Z
author_association: COLLABORATOR
diff_hunk: @@ -2,9 +2,9 @@
 // MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
 // ------------------------------------------------------------------------
 
-using Microsoft.AspNetCore.Components;
path: src/Core/Extensions/FluentInputExtensions.cs
user: login: dvoituron
body: Why did you changed that?
created_at: 2025-04-14T18:51:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+﻿@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@using System.Diagnostics
+@inherits FluentComponentBase
+@typeparam TValue
+@{
+    Debug.Assert(Context != null);
+}
+<FluentField ForId="@Id"
+             Class="@ClassValue"
+             Style="@StyleValue"
+             Label="@Label"
+             LabelPosition="@Components.LabelPosition.After"
+             LabelWidth="@LabelWidth"
+             IncludeInputSlot="false">
+    <LabelTemplate>@LabelTemplate</LabelTemplate>
+    <ChildContent>
+        <fluent-radio id="@Id"
+                      checked="@(Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null)"
+                      disabled="@Disabled"
+                      slot="@FluentSlot.FieldInput"
+                      value="@(Value?.ToString() ?? Label ?? null)"
path: src/Core/Components/Radio/FluentRadio.razor
user: login: dvoituron
body: If the value is a "complex" object, this result will be a very long string of this object :-(
created_at: 2025-04-14T18:53:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+﻿@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@using System.Diagnostics
+@inherits FluentComponentBase
+@typeparam TValue
+@{
+    Debug.Assert(Context != null);
+}
+<FluentField ForId="@Id"
+             Class="@ClassValue"
+             Style="@StyleValue"
+             Label="@Label"
+             LabelPosition="@Components.LabelPosition.After"
+             LabelWidth="@LabelWidth"
+             IncludeInputSlot="false">
+    <LabelTemplate>@LabelTemplate</LabelTemplate>
+    <ChildContent>
+        <fluent-radio id="@Id"
+                      checked="@(Context.CurrentValue?.Equals(Value) == true ? GetToggledTrueValue() : null)"
+                      disabled="@Disabled"
+                      slot="@FluentSlot.FieldInput"
+                      value="@(Value?.ToString() ?? Label ?? null)"
path: src/Core/Components/Radio/FluentRadio.razor
user: login: vnbaaij
body: As discussed, will be addressed in a future version
created_at: 2025-04-14T19:06:45Z
author_association: COLLABORATOR
diff_hunk: @@ -2,9 +2,9 @@
 // MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
 // ------------------------------------------------------------------------
 
-using Microsoft.AspNetCore.Components;
path: src/Core/Extensions/FluentInputExtensions.cs
user: login: vnbaaij
body: System references are placed on top
created_at: 2025-04-14T19:10:50Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-08T21:47:59Z
body: ✅ All tests passed successfully

 Details on [your Workflow / Core Tests](/microsoft/fluentui-blazor/actions/workflows/build-core-lib.yml) page.

<!-- Sticky Pull Request CommentTest Results -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-10T21:27:06Z
body: # Summary - Unit Tests Code Coverage
<details open><summary>Summary</summary>

|||
|:---|:---|
| Generated on: | 04/15/2025 - 06:54:35 |
| Coverage date: | 04/15/2025 - 06:54:19 |
| Parser: | Cobertura |
| Assemblies: | 1 |
| Classes: | 110 |
| Files: | 144 |
| **Line coverage:** | 99.3% (2566 of 2582) |
| Covered lines: | 2566 |
| Uncovered lines: | 16 |
| Coverable lines: | 2582 |
| Total lines: | 11374 |
| **Branch coverage:** | 93.4% (1462 of 1564) |
| Covered branches: | 1462 |
| Total branches: | 1564 |
| **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) |
| Tag: | 3355_14463163523 |

</details>

## Coverage
<details><summary>Microsoft.FluentUI.AspNetCore.Components - 99.3%</summary>

|**Name**|**Line**|**Branch**|
|:---|---:|---:|
|**Microsoft.FluentUI.AspNetCore.Components**|**99.3%**|**93.4%**|
|Microsoft.FluentUI.AspNetCore.Components.AccordionItemEventArgs|100%||
|Microsoft.FluentUI.AspNetCore.Components.AdditionalAttributesExtensions|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.CachedServices|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.DefaultStyles|100%||
|Microsoft.FluentUI.AspNetCore.Components.Dialog.MessageBox.FluentMessageBox|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.DialogEventArgs|100%|92.8%|
|Microsoft.FluentUI.AspNetCore.Components.DialogInstance|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.DialogOptions|100%||
|Microsoft.FluentUI.AspNetCore.Components.DialogOptionsFooter|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.DialogOptionsFooterAction|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.DialogOptionsHeader|100%||
|Microsoft.FluentUI.AspNetCore.Components.DialogResult|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.DialogResult`1|100%||
|Microsoft.FluentUI.AspNetCore.Components.DialogService|98.9%|81.4%|
|Microsoft.FluentUI.AspNetCore.Components.DialogToggleEventArgs|100%||
|Microsoft.FluentUI.AspNetCore.Components.DropdownEventArgs|100%||
|Microsoft.FluentUI.AspNetCore.Components.Extensions.EnumExtensions|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.Extensions.FieldSizeExtensions|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.Extensions.FluentInputExtensions|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentAccordion|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentAccordionItem|100%|95%|
|Microsoft.FluentUI.AspNetCore.Components.FluentAnchorButton|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentAvatar|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentBadge|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentButton|98.4%|90.6%|
|Microsoft.FluentUI.AspNetCore.Components.FluentCheckbox|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentCombobox`1|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentComponentBase|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentCompoundButton|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentCounterBadge|100%|96.1%|
|Microsoft.FluentUI.AspNetCore.Components.FluentDialog|97.4%|89.7%|
|Microsoft.FluentUI.AspNetCore.Components.FluentDialogBody|100%|91.6%|
|Microsoft.FluentUI.AspNetCore.Components.FluentDialogInstance|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentDialogProvider|100%|80%|
|Microsoft.FluentUI.AspNetCore.Components.FluentDivider|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentField|100%|96.6%|
|Microsoft.FluentUI.AspNetCore.Components.FluentFieldCondition|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentFieldConditionItem|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentFieldConditionOptions|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentFieldExtensions|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentFieldParameterSelector|100%|98.7%|
|Microsoft.FluentUI.AspNetCore.Components.FluentGrid|100%|90%|
|Microsoft.FluentUI.AspNetCore.Components.FluentGridItem|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentIcon`1|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentImage|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentInputBase`1|93.7%|43.7%|
|Microsoft.FluentUI.AspNetCore.Components.FluentInputImmediateBase`1|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentJSModule|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentLabel|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentLayout|100%|85%|
|Microsoft.FluentUI.AspNetCore.Components.FluentLayoutHamburger|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentLayoutItem|100%|87.7%|
|Microsoft.FluentUI.AspNetCore.Components.FluentLink|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentListBase`1|98.4%|92.8%|
|Microsoft.FluentUI.AspNetCore.Components.FluentLocalizerExtensions|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentLocalizerInternal|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentMenu|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentMenuButton|100%|71.4%|
|Microsoft.FluentUI.AspNetCore.Components.FluentMenuItem|100%|90.4%|
|Microsoft.FluentUI.AspNetCore.Components.FluentMenuList|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentOption|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentProgressBar|100%|95.4%|
|Microsoft.FluentUI.AspNetCore.Components.FluentProviders|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentRadio`1|100%|50%|
|Microsoft.FluentUI.AspNetCore.Components.FluentRadioContext|100%|75%|
|Microsoft.FluentUI.AspNetCore.Components.FluentRadioGroup`1|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentRatingDisplay|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentSelect`1|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentServiceBase`1|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentServiceProviderException`1|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentSlider`1|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentSpacer|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentSpinner|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentSplitButton|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentStack|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentStatus|100%||
|Microsoft.FluentUI.AspNetCore.Components.FluentSwitch|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentTab|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentTabs|100%|97.7%|
|Microsoft.FluentUI.AspNetCore.Components.FluentText|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FluentTextArea|100%|66.6%|
|Microsoft.FluentUI.AspNetCore.Components.FluentTextInput|100%|78.5%|
|Microsoft.FluentUI.AspNetCore.Components.FluentToggleButton|100%|91.6%|
|Microsoft.FluentUI.AspNetCore.Components.FluentTooltip|100%|95%|
|Microsoft.FluentUI.AspNetCore.Components.FluentTooltipProvider|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.FreeOptionOutput|100%||
|Microsoft.FluentUI.AspNetCore.Components.Icon|100%|89.4%|
|Microsoft.FluentUI.AspNetCore.Components.IconFromImage|100%||
|Microsoft.FluentUI.AspNetCore.Components.IconInfo|100%||
|Microsoft.FluentUI.AspNetCore.Components.IFluentLocalizer|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.InternalListContext`1|100%||
|Microsoft.FluentUI.AspNetCore.Components.LibraryTooltipOptions|100%||
|Microsoft.FluentUI.AspNetCore.Components.Localization.LanguageResource|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.MenuItemEventArgs|100%||
|Microsoft.FluentUI.AspNetCore.Components.MessageBoxOptions|100%||
|Microsoft.FluentUI.AspNetCore.Components.Migration.AppearanceExtensions|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.Migration.FluentInputAppearanceExt<br/>ensions|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.Migration.TooltipPositionExtension|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.ServiceProviderExtensions|100%||
|Microsoft.FluentUI.AspNetCore.Components.SpacingExtensions|100%|97.2%|
|Microsoft.FluentUI.AspNetCore.Components.TabChangeEventArgs|100%||
|Microsoft.FluentUI.AspNetCore.Components.TooltipEventArgs|100%||
|Microsoft.FluentUI.AspNetCore.Components.Utilities.AddTag|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.Utilities.CssBuilder|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.Utilities.Identifier|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.Utilities.IdentifierContext|100%|75%|
|Microsoft.FluentUI.AspNetCore.Components.Utilities.InlineStyleBuilder|100%|92.8%|
|Microsoft.FluentUI.AspNetCore.Components.Utilities.InternalDebounce.Dispatc<br/>herTimerExtensions|82.9%|68.7%|
|Microsoft.FluentUI.AspNetCore.Components.Utilities.StyleBuilder|100%|100%|
|Microsoft.FluentUI.AspNetCore.Components.ZIndex|100%||

</details>

<!-- Sticky Pull Request Comment -->
author_association: NONE