I am trying to customize the appearance of a RadioButton in .NET MAUI, with the goal of dynamically changing the text color based on its selection state (Checked/Unchecked) and using the button's background as a visual indicator for selection. However, I encountered a challenge: I cannot apply the text color directly to the container element (like a Grid or Border) because they do not have a TextColor property.
How can I achieve this effect while ensuring the RadioButton has no checkmark or other icon?
Any help would be greatly appreciated!
<Style TargetType="RadioButton" x:Key="WizardRadioButton">
<Setter Property="FontFamily" Value="Inter-Bold"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="0,6,0,6"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}"/>
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="ControlTemplate">
<Setter.Value>
<ControlTemplate>
<Border StrokeThickness="0"
Stroke="Transparent"
Padding="10,5"
BackgroundColor="Transparent"
StrokeShape="RoundRectangle 8">
<ContentPresenter VerticalOptions="Center"
HorizontalOptions="Center"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CheckedStates">
<VisualState x:Name="Checked">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#1CB41C"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unchecked">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I am trying to customize the appearance of a RadioButton in .NET MAUI, with the goal of dynamically changing the text color based on its selection state (Checked/Unchecked) and using the button's background as a visual indicator for selection. However, I encountered a challenge: I cannot apply the text color directly to the container element (like a Grid or Border) because they do not have a TextColor property.
How can I achieve this effect while ensuring the RadioButton has no checkmark or other icon?
Any help would be greatly appreciated!
<Style TargetType="RadioButton" x:Key="WizardRadioButton">
<Setter Property="FontFamily" Value="Inter-Bold"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="0,6,0,6"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}"/>
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="ControlTemplate">
<Setter.Value>
<ControlTemplate>
<Border StrokeThickness="0"
Stroke="Transparent"
Padding="10,5"
BackgroundColor="Transparent"
StrokeShape="RoundRectangle 8">
<ContentPresenter VerticalOptions="Center"
HorizontalOptions="Center"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CheckedStates">
<VisualState x:Name="Checked">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#1CB41C"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unchecked">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Share
Improve this question
edited Feb 16 at 6:44
marc_s
755k184 gold badges1.4k silver badges1.5k bronze badges
asked Feb 15 at 22:09
FelixFelix
211 silver badge1 bronze badge
New contributor
Felix is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
1
- What platform are you targeting on? – Liqun Shen-MSFT Commented Feb 19 at 7:43
2 Answers
Reset to default 1The ContentPresenter
seems to be a Label
. So, what you can define a ResourceDictionary
in your ContentPresenter
to bind Label.TextColor
to the Border.Stroke.Color
:
<Style x:Key="WizardRadioButton" TargetType="RadioButton">
<Setter Property="FontFamily" Value="Inter-Bold" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0,6,0,6" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="ControlTemplate">
<Setter.Value>
<ControlTemplate>
<Border
Padding="10,5"
BackgroundColor="Transparent"
Stroke="Transparent"
StrokeShape="RoundRectangle 8"
StrokeThickness="0">
<ContentPresenter HorizontalOptions="Center" VerticalOptions="Center">
<ContentPresenter.Resources>
<ResourceDictionary>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{Binding Stroke.Color, x:DataType=Border, Source={RelativeSource AncestorType={x:Type Border}}}" />
</Style>
</ResourceDictionary>
</ContentPresenter.Resources>
</ContentPresenter>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CheckedStates">
<VisualState x:Name="Checked">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#1CB41C" />
<Setter Property="Stroke" Value="White" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unchecked">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Stephen’s answer ▲ does a great job using a Style
, as the OP requested. To build on that, this answer drills a little deeper into an alternative approach: a custom control for better reusability, maintainability, and extensibility across projects. Since the OP wants a control that "has no checkmark or other icon" (essentially a Button
), what if we create OneHotButton
and give it a property that binds in XAML markup—e.g., GroupName
, whose members behave one-hot, just like a RadioButton
group? While we're at it, we can add bindable properties for SelectedTextColor
, SelectedBackgroundColor
, UnselectedTextColor
, and UnselectedBackgroundColor
, making it easier to customize the appearance without modifying multiple styles.
<local:OneHotButton Text="Apple" SelectedTextColor="Salmon" GroupName="OptionsGroup" IsChecked="true"/>
and
<Style TargetType="local:OneHotButton" >
<Setter Property="SelectedBackgroundColor" Value="CornflowerBlue"/>
</Style>
Example One Hot Implementation
[DebuggerDisplay("{Text}")]
public partial class OneHotButton : Button
{
public OneHotButton()
{
Clicked += OnButtonClicked;
UpdateColors();
}
private static readonly Dictionary<string, List<OneHotButton>> buttonGroups = new();
public static readonly BindableProperty GroupNameProperty =
BindableProperty.Create(
nameof(GroupName),
typeof(string),
typeof(OneHotButton),
default(string),
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is OneHotButton button)
{
if (oldValue is string oldGroup)
{
buttonGroups[oldGroup].Remove(button);
if (buttonGroups[oldGroup].Count == 0)
buttonGroups.Remove(oldGroup);
}
if (newValue is string newGroup && !string.IsNullOrWhiteSpace(newGroup))
{
if (!buttonGroups.ContainsKey(newGroup))
buttonGroups[newGroup] = new List<OneHotButton>();
buttonGroups[newGroup].Add(button);
}
}
});
public static readonly BindableProperty IsCheckedProperty =
BindableProperty.Create(
nameof(IsChecked),
typeof(bool),
typeof(OneHotButton),
false,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is OneHotButton button &&
Equals(newValue, true) &&
!string.IsNullOrWhiteSpace(button.GroupName) &&
buttonGroups.TryGetValue(button.GroupName, out var buttonList))
{
button.UpdateColors();
buttonList
.Where(_ => !ReferenceEquals(_, button))
.ToList()
.ForEach(_ =>
{
_.IsChecked = false;
_.UpdateColors();
});
}
});
...
}
Example Bindable Color
This snippet shows how to expose a property to be used in XAML markup.
public Color SelectedTextColor
{
get => (Color)GetValue(SelectedTextColorProperty);
set => SetValue(SelectedTextColorProperty, value);
}
public static readonly BindableProperty SelectedBackgroundColorProperty =
BindableProperty.Create(
propertyName: nameof(OneHotButton.SelectedBackgroundColor),
returnType: typeof(Color),
declaringType: typeof(OneHotButton),
defaultValue: Colors.CornflowerBlue,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is OneHotButton @this)
{
@this.UpdateColors();
}
});
private void UpdateColors()
{
if(IsChecked)
{
TextColor = SelectedTextColor;
BackgroundColor = SelectedBackgroundColor;
}
else
{
TextColor = UnselectedTextColor;
BackgroundColor = UnselectedBackgroundColor;
}
}
... Other bindable colors
XAML Example
<ContentPage xmlns="http://schemas.microsoft/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft/winfx/2009/xaml"
xmlns:local="clr-namespace:style_radio_group"
x:Class="style_radio_group.MainPage">
<ContentPage.Resources>
<Style TargetType="local:OneHotButton" >
<Setter Property="SelectedBackgroundColor" Value="CornflowerBlue"/>
<Setter Property="Margin" Value="25,0"/>
</Style>
</ContentPage.Resources>
<ScrollView>
<VerticalStackLayout Padding="30,0" Spacing="25">
<Image
Source="dotnet_bot.png"
HeightRequest="185"
Aspect="AspectFit"
SemanticProperties.Description="dot net bot in a race car number eight" />
<Border Padding="0,0,0,10" StrokeShape="RoundRectangle 10">
<VerticalStackLayout Spacing="10">
<Label
Text="Choose One Fruit"
Padding="5"
BackgroundColor="LightGray"
FontSize="16"
FontAttributes="Bold"
HorizontalOptions="Fill"/>
<local:OneHotButton
Text="Apple"
SelectedTextColor="Salmon"
GroupName="OptionsGroup"
IsChecked="true"/>
<local:OneHotButton
Text="Banana"
SelectedTextColor="Yellow"
GroupName="OptionsGroup"/>
<local:OneHotButton
Text="Grape"
SelectedTextColor="LightGreen"
GroupName="OptionsGroup"/>
</VerticalStackLayout>
</Border>
</VerticalStackLayout>
</ScrollView>
</ContentPage>