Create TabBar in MAUI

In this post, I show you how to create a nice TabBar in MAUI without using any external NuGet package or components. Fully customizable and 100% XAML.

Here is the result of the code:

MAUI TabBar in Windows - Create TabBar in MAUI
MAUI TabBar in Windows
MAUI Tabs in Android - Create TabBar in MAUI
MAUI TabBar in Android
MAUI Tabs in iOS - Create TabBar in MAUI
MAUI Tabs in iOS

The source code of this post is on GitHub.

Bindable Layout

.NET MAUI bindable layouts enable any layout class that derives from the Layout class to generate its content by binding to a collection of items, with the option to set the appearance of each item with a DataTemplate.

Bindable layouts are provided by the BindableLayout class, which exposes the following attached properties:

  • ItemsSource – specifies the collection of IEnumerable items to be displayed by the layout.
  • ItemTemplate – specifies the DataTemplate to apply to each item in the collection of items displayed by the layout.
  • ItemTemplateSelector – specifies the DataTemplateSelector that will be used to choose a DataTemplate for an item at runtime.
  • In addition, the BindableLayout class exposes the following bindable properties:
  • EmptyView – specifies the string or view that will be displayed when the ItemsSource property is null, or when the collection specified by the ItemsSource property is null or empty. The default value is null.
  • EmptyViewTemplate – specifies the DataTemplate that will be displayed when the ItemsSource property is null, or when the collection specified by the ItemsSource property is null or empty. The default value is null.

Populate a bindable layout with data

A bindable layout is populated with data by setting its ItemsSource property to any collection that implements IEnumerable, and attaching it to a Layout-derived class:

<Grid BindableLayout.ItemsSource="{Binding Items}" />

When the BindableLayout.ItemsSource attached property is set on a layout, but the BindableLayout.ItemTemplate attached property isn’t set, every item in the IEnumerable collection will be displayed by a Label that’s created by the BindableLayout class.

Radiobutton

The .NET Multi-platform App UI (.NET MAUI) RadioButton is a type of button that allows users to select one option from a set. Each option is represented by one radio button, and you can only select one radio button in a group. By default, each RadioButton displays text:

Screenshot of RadioButtons.

However, on some platforms a RadioButton can display a View, and on all platforms the appearance of each RadioButton can be redefined with a ControlTemplate:

Screenshot of re-defined RadioButtons.

RadioButton defines the following properties:

  • Content, of type object, which defines the string or View to be displayed by the RadioButton.
  • IsChecked, of type bool, which defines whether the RadioButton is checked. This property uses a TwoWay binding, and has a default value of false.
  • GroupName, of type string, which defines the name that specifies which RadioButton controls are mutually exclusive. This property has a default value of null.
  • Value, of type object, which defines an optional unique value associated with the RadioButton.
  • BorderColor, of type Color, which defines the border stroke color.
  • BorderWidth, of type double, which defines the width of the RadioButton border.
  • CharacterSpacing, of type double, which defines the spacing between characters of any displayed text.
  • CornerRadius, of type int, which defines the corner radius of the RadioButton.
  • FontAttributes, of type FontAttributes, which determines text style.
  • FontAutoScalingEnabled, of type bool, which defines whether an app’s UI reflects text scaling preferences set in the operating system. The default value of this property is true.
  • FontFamily, of type string, which defines the font family.
  • FontSize, of type double, which defines the font size.
  • TextColor, of type Color, which defines the color of any displayed text.
  • TextTransform, of type TextTransform, which defines the casing of any displayed text.

These properties are backed by BindableProperty objects, which means that they can be targets of data bindings, and styled.

RadioButton also defines a CheckedChanged event that’s raised when the IsChecked property changes, either through user or programmatic manipulation. The CheckedChangedEventArgs object that accompanies the CheckedChanged event has a single property named Value, of type bool. When the event is raised, the value of the CheckedChangedEventArgs.Value property is set to the new value of the IsChecked property.

RadioButton grouping can be managed by the RadioButtonGroup class, which defines the following attached properties:

  • GroupName, of type string, which defines the group name for RadioButton objects in an ILayout.
  • SelectedValue, of type object, which represents the value of the checked RadioButton object within an ILayout group. This attached property uses a TwoWay binding by default.

For more information about the GroupName attached property, see Group RadioButtons. For more information about the SelectedValue attached property, see Respond to RadioButton state changes.

The UI implementation

First, I created my container with a set of data to be repeated for each tab.

<HorizontalStackLayout>
    <BindableLayout.ItemTemplate>
    <!-- Radiobutton here -->
    </BindableLayout.ItemTemplate>
    <BindableLayout.ItemsSource>
        <x:Array Type="{x:Type x:String}">
            <x:String>Hot Dishes</x:String>
            <x:String>Cold Dishes</x:String>
            <x:String>Soups</x:String>
            <x:String>Appetizers</x:String>
            <x:String>Desserts</x:String>
        </x:Array>
    </BindableLayout.ItemsSource>
</HorizontalStackLayout>

Then, I add the RadioButton

<BindableLayout.ItemTemplate>
    <DataTemplate>
        <RadioButton Content="{Binding .}" />
    </DataTemplate>
</BindableLayout.ItemTemplate>

To make this a grouped list of radios, I added a name to the parent layout.

<HorizontalStackLayout 
    RadioButtonGroup.GroupName="MenuCategories">

Add a style

Then, I styled the RadioButton using a ControlTemplate.

<RadioButton Content="{Binding .}">
    <RadioButton.ControlTemplate>
        <ControlTemplate>
            <Grid RowDefinitions="30,4">
                <Label Text="{TemplateBinding Content}" />
                <BoxView Grid.Row="1" Color="Transparent"/>
            </Grid>
        </ControlTemplate>
    </RadioButton.ControlTemplate>
</RadioButton>

Because we are inside of a control template, rather than using Binding I use TemplateBinding. The Content could be anything, but I supplied a String so it seems safe to bind that directly to the label text.

Now, to get a different look for selected vs unselected, I:

  • added a VisualStateManager (VSM) to the control template layout
  • gave my label and box names so I could target them from the VSM
  • styled the checked and unchecked states
<ControlTemplate>
    <Grid RowDefinitions="30,4">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroupList>
                <VisualStateGroup x:Name="CheckedStates">
                    <VisualState x:Name="Checked">
                        <VisualState.Setters>
                            <Setter
                                TargetName="TextLabel"
                                Property="Label.TextColor"
                                Value="{StaticResource Primary}"/>
                            <Setter
                                TargetName="Indicator"
                                Property="BoxView.Color"
                                Value="{StaticResource Primary}"/>
                        </VisualState.Setters>
                    </VisualState>

                    <VisualState x:Name="Unchecked">
                        <VisualState.Setters>
                            <Setter
                                TargetName="TextLabel"
                                Property="Label.TextColor"
                                Value="White"/>
                            <Setter
                                TargetName="Indicator"
                                Property="BoxView.Color"
                                Value="Transparent"/>
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateGroupList>
        </VisualStateManager.VisualStateGroups>
        <Label Text="{TemplateBinding Content}" x:Name="TextLabel" />
        <BoxView x:Name="Indicator" Grid.Row="1" Color="Transparent"/>
    </Grid>
</ControlTemplate>

Binding with a ViewModel

Now, I have a nice and easy way to display tabs but I want to bind the UI with my data that are coming from the ViewModel.

First, I add a model for the items called MenuItem

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MauiTabs
{
    /// <summary>
    /// Class MenuItem.
    /// </summary>
    public class MenuItem
    {
        /// <summary>
        /// Gets or sets the text.
        /// </summary>
        /// <value>The text.</value>
        public string? Text { get; set; }

        /// <summary>
        /// Gets or sets the value.
        /// </summary>
        /// <value>The value.</value>
        public string? Value { get; set; }
    }
}

Create the ViewModel

Now, using the CommunityToolkit I’m going to create the ViewModel as in the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;

namespace MauiTabs
{
    public partial class MainPageViewModel : ObservableObject
    {
        [ObservableProperty] private List<MenuItem>? _menuItems;
        [ObservableProperty] private string? _selected;

        public MainPageViewModel()
        {
            MenuItems = new List<MenuItem>
            {
                new MenuItem { Text = "Words", Value = "Words" },
                new MenuItem { Text = "Games", Value = "Games" },
                new MenuItem { Text = "Statistics", Value = "Statistics" }
            };

            Selected = MenuItems[0].Value;
        }
    }
}

In line 13, I define the selected or default value. In this variable, the application will save the choice of the user. It is a simple string that will contain the Value of the MenuItem.

Then, in line 24, I define by default that the default element alias the element I want to highlight when the page will appear is the first one.

Bind the ViewModel

Now, in the page, in my case _MainPage_, I’m going to bind the ViewModel to the page itself like that:

namespace MauiTabs
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            BindingContext = new MainPageViewModel();
        }
    }
}

Change the XAML

After everything, I have to change the XAML to display the text and the value for the ViewModel.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
	x:Class="MauiTabs.MainPage"
	xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
	xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	xmlns:dt="clr-namespace:MauiTabs">

	<ScrollView>
		<VerticalStackLayout>
			<HorizontalStackLayout BindableLayout.ItemsSource="{Binding MenuItems}"
								   RadioButtonGroup.GroupName="MenuCategories" 
								   RadioButtonGroup.SelectedValue="{Binding Selected}">
				<BindableLayout.ItemTemplate>
					<DataTemplate x:DataType="dt:MenuItem">
						<RadioButton
							x:Name="radio"
							Margin="0,0,15,0"
							Content="{Binding Text}"
							GroupName="MenuCategories"
							Value="{Binding Value}">
							<RadioButton.ControlTemplate>
								<ControlTemplate>
									<Grid RowDefinitions="30,4">
										<Label x:Name="TextLabel" Text="{TemplateBinding Content}" />
										<BoxView
											x:Name="Indicator"
											Grid.Row="1"
											Color="Transparent" />
										<VisualStateManager.VisualStateGroups>
											<VisualStateGroupList>
												<VisualStateGroup x:Name="CheckedStates">
													<VisualState x:Name="Checked">
														<VisualState.Setters>
															<Setter TargetName="TextLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
															<Setter TargetName="Indicator" Property="BoxView.Color" Value="{StaticResource Primary}" />
														</VisualState.Setters>
													</VisualState>

													<VisualState x:Name="Unchecked">
														<VisualState.Setters>
															<Setter TargetName="TextLabel" Property="Label.TextColor" Value="Black" />
															<Setter TargetName="Indicator" Property="BoxView.Color" Value="Transparent" />
														</VisualState.Setters>
													</VisualState>
												</VisualStateGroup>
											</VisualStateGroupList>
										</VisualStateManager.VisualStateGroups>
									</Grid>
								</ControlTemplate>
							</RadioButton.ControlTemplate>
						</RadioButton>
					</DataTemplate>
				</BindableLayout.ItemTemplate>
			</HorizontalStackLayout>
			<HorizontalStackLayout>
				<Label Text="Your selection:" />
				<Label x:Name="labelSelection" Text="{Binding Selected}" />
			</HorizontalStackLayout>
		</VerticalStackLayout>
	</ScrollView>
</ContentPage>

In line 12, I bind the variable Selected to the RadioButtonGroup.SelectedValue. The first time the page is displayed, the first RadioButton is selected as expected. When a user taps on the others, the Selected value will change with the defined Value.

In lines 18 and 20, I bind the Text to display and the Value for each element. Remember to set the GroupName because instead it will not work.

Finally, in line 57, the Selected value will be display.

Wrap up

In conclusion, I hope this code will help you to create a nice TabBar in MAUI without using third-party components and full-customized as you like. Let me know what you think in the comment or in the Forum.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.