WPF Standard Control Demo App › TabItem
TabItem Selectors
TabItem is one page of a TabControl: a header shown in the tab strip, and content shown while the tab is selected.
Which tab ends up selected
TabItem derives from HeaderedContentControl, so it has a Header (the label in the tab strip) and a Content (the page). The parent TabControl selects at most one tab at a time: setting IsSelected to True on one TabItem moves SelectedIndex to it and clears IsSelected on the others. Setting IsSelected="True" on the third of three tabs gave SelectedIndex 2 and cleared the others.
IsSelected binds two-way by default. Changing SelectedIndex to 1 set the first bound source to False and the second to True, and so did a click on the second tab’s header with the real mouse.
Only one tab can stay selected, and which one wins depends on how the values arrive. Setting two tabs to True one after the other from code left the last one selected. Writing IsSelected="True" on two tabs in XAML left the first one selected. Select with one source of truth: bind SelectedIndex or SelectedItem on the TabControl instead of IsSelected on every tab.
A disabled tab can still be selected from code
A tab with IsEnabled="False" cannot be selected by the user. Selecting it with ISelectionItemProvider.Select, the path an assistive tool uses, threw ElementNotEnabledException, and the selection did not move. A click on its header with the real mouse did not select it either.
Code is not blocked: SelectedIndex = 1 selected the disabled tab and displayed its content. If later tabs stay disabled until earlier steps are complete, keep the code that changes the selection from choosing a tab that must stay closed.
| case | measured |
|---|---|
| base class | HeaderedContentControl |
| IsSelected: BindsTwoWayByDefault | True |
| TabStripPlacement on TabItem: read-only | True |
| TabControl.TabStripPlacement=Left: TabItem.TabStripPlacement of both tabs | Left / Left |
| Tab3.IsSelected = true: SelectedIndex / IsSelected of Tab1..3 | 2 / False, False, True |
| XAML: IsSelected="True" on Tab2 and Tab3: SelectedIndex / IsSelected of Tab1..3 | 1 / False, True, False |
| code: Tab2 then Tab3 set to IsSelected=true: SelectedIndex | 2 |
| IsSelected bound without Mode; SelectedIndex = 1: source values of Tab1 / Tab2 | False / True |
| then Tab1's source set to True: SelectedIndex / source values of Tab1 / Tab2 | 0 / True / False |
| then Tab2's header clicked with the real mouse: SelectedIndex / sources of Tab1 / Tab2 | 1 / False / True |
| Tab2 IsEnabled=False; SelectedIndex = 1 from code: SelectedIndex / content shown | 1 / Item2 |
| Tab2 IsEnabled=False; UI Automation ISelectionItemProvider.Select(): result / SelectedIndex | ElementNotEnabledException / 0 |
| Tab2 IsEnabled=False; header clicked with the real mouse: SelectedIndex | 0 |
Selection, metadata, and disabled tabs. Measured on .NET 10 / Windows 11.
When the content is created, and when it is reused
Only the selected tab’s content is shown and laid out, but that does not mean the other tabs’ content is idle. Content written directly in XAML is created when the XAML is loaded. In a test with three tabs, all three content elements raised Loaded when the window opened, and only the selected one was measured. Switching from tab 1 to tab 2 and back raised Loaded again on the content that came back, and the same instance was shown again. To defer expensive work, start it when the tab is first selected, not in the content’s constructor or Loaded handler.
With ItemsSource and a ContentTemplate, the TabControl created one content element and reused it for every tab, changing only its data. State that is not bound to the item, such as a scroll position or a text box that is not bound, therefore carries over when the user switches tabs.
For generated tabs, the headers come from ItemTemplate. TabControl has no HeaderTemplate property. The templates you set on it are ItemTemplate (headers), ContentTemplate (pages), and Template. SelectedContentTemplate is read-only: it returns the template used by the selected tab. HeaderTemplate exists on TabItem.
| how the content is given | measured | same instance after switching back |
|---|---|---|
| elements as TabItem.Content: Loaded count per tab | at start 1 / 1 / 1; after 1 -> 2 -> 1: 2 / 2 / 1 | True |
| same, measure calls per tab at start | 1 / 0 / 0 | - |
| ItemsSource + ContentTemplate: content instances created | at start 1 created; after 1 -> 2 -> 1: 1 created | True |
When tab content is loaded and whether it is reused, for three tabs switched 1 → 2 → 1. Measured on .NET 10 / Windows 11.
The header, the strip position, and the template
A string header is displayed through an AccessText, so an underscore marks an access key. Header="File_Name" made N the access key, and the underscore itself is not displayed. Why WPF Label Hides Underscores and How to Fix It confirms that the underscore disappears from TabItem headers as well. It also shows that writing __ in a Label, which uses the same AccessText, displays a single underscore. The tab width follows the header: on the measuring machine, “File_Name” gave a tab 65.65 wide and “A” one 19.74 wide (the widths depend on the font and display scaling).
TabStripPlacement on a TabItem is read-only and follows the parent TabControl. With TabControl.TabStripPlacement="Left", both TabItems reported Left. Setting it is done on the TabControl; on the TabItem it is for templates and triggers that need to know which edge the strip is on.
The default TabItem template has no visual state groups; it changes its look with 16 triggers. A copied default template is edited through those triggers.
Trying it in the demo app

The TabItem page of the demo app has two sections. In the IsSelected section, a check box is bound to each tab’s IsSelected without a Mode, so clicking a tab also updates the check boxes. In the TabStripPlacement section, a combo box moves the tab strip, and each TabItem shows its own read-only TabStripPlacement. The “Show Code” link under each section displays its XAML. The following XAML is the IsSelected section (TabItemUsageControl.xaml), with the styles and the surrounding GroupBoxes left out and the namespace declarations added:
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CheckBox x:Name="IsSelectedTab1" VerticalContentAlignment="Center" Content="Tab1" />
<CheckBox x:Name="IsSelectedTab2" VerticalContentAlignment="Center" Content="Tab2" />
<TabControl x:Name="IsSelectedTabControl">
<TabItem Content="Item1" Header="Tab1"
IsSelected="{Binding IsChecked, ElementName=IsSelectedTab1}" />
<TabItem Content="Item2" Header="Tab2"
IsSelected="{Binding IsChecked, ElementName=IsSelectedTab2}" />
</TabControl>
</StackPanel>
Because IsSelected binds two-way by default, the check boxes follow the tabs as well as control them. With Tab2 selected, setting Tab1’s source to True (what checking its box does) selected Tab1 and set Tab2’s source to False.
Related controls and articles
- TabControl — the parent that selects one TabItem at a time and shows its content.
- Why WPF Label Hides Underscores and How to Fix It — the access-key behavior that also applies to TabItem headers.
Source code and how it was measured
Every behavior on this page was measured by running it on .NET 10 / Windows 11, using TabItemDemoScene in this site’s screenshot tool. Clicks on tab headers were made with the real mouse over the measuring window.