Revit menu (UI) custom items: ribbon tabs, panels, buttons, textBoxes, comboBoxes

You can add your own panels, tabs, buttons, and other controls to Revit menu when creating a Revit plugin. You can customize the controls with images, tooltips, or extended contextual help. However, especially in the new Revit 2025 .NET API, there are some caveats.
📌 TLDR – Summary Upfront:
Modify the Revit menu (e.g., add your own ribbon tabs and panels) using the OnStartup method of your Application class.
➡️
To create a Button, define the PushButtonData with name, text, and linked command.
➡️
To create a TextBox, define the TextBoxData with name.
➡️
To create a ComboBox, define the ComboBoxData with name and populate it with ComboBoxMembers.
You can customize controls with, e.g., tooltips, contextual help, and images (tricky for Revit 2025 API).
To retrieve inserted values, use events: EnterPressed for TextBoxes and CurrentChanged for ComboBoxes.

Where to start – IExternalApplication.OnStartup

To modify the Revit menu, you need the Application plugin type. Implement the IExternalApplication interface and use the OnStartup method to insert your menu-modifying code, such as creating a ribbon tabs and panels.
To get along with the whole plugin creation process, check the Start Revit API 2025 c# Programming Guide.
Example: Creating a ribbon tab called “Revit Mastery” and a panel.
C#
using System.IO;
using System.Reflection;
using System.Windows.Media.Imaging;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;

namespace RevitMastery.Revit
{
	[Transaction(TransactionMode.Manual)]
	[Regeneration(RegenerationOption.Manual)]
	public class RevitMasteryApplication : IExternalApplication
	{
		public Result OnStartup(UIControlledApplication application)
		{
			var ribbonTabName = "Revit Mastery";
			application.CreateRibbonTab(ribbonTabName);
			var panel = application.CreateRibbonPanel(ribbonTabName, "Revit Mastery Panel");
			
			return Result.Succeeded;
		}
		
		public Result OnShutdown(UIControlledApplication application)
		{
			return Result.Succeeded;
		}
	}
}
Now, you can add controls to the panel.

Buttons: creating and customizing

Buttons are the most popular and most useful control.
Start by defining a PushButtonData providing 4 attributes:
name: the button’s ID. It is not visible in the Revit menu and must be unique. I provide the same as the className.
text: the button name displayed in the Revit menu.
assemblyName: the location of the currently executing assembly (the same for each control, so you can simply copy it as boilerplate code).
className: command to execute when the button is clicked.
C#
	[Transaction(TransactionMode.Manual)]
	[Regeneration(RegenerationOption.Manual)]
	public class ShowElementsDataCommand : IExternalCommand
	{
		public Result Execute(ExternalCommandData commandData, ref string message,
		ElementSet elements)
		{				
			TaskDialog.Show("Show elements data", "The button has been clicked.");
			return Result.Succeeded;
		}
	}
After defining the PushButtonData, add it to a panel to create the actual PushButton.
Then, you can add a ToolTip and ContextualHelp (with, e.g., a website link).
C#
var assemblyPath = Assembly.GetExecutingAssembly().Location; //localization of the currently executing assembly. Don't worry, copy ;)
var assemblyDirectory = Path.GetDirectoryName(assemblyPath);

// Button
var buttonData = new PushButtonData(
  typeof(ShowElementsDataCommand).FullName,
	"Show elements data", 
	assemblyPath,
	typeof(ShowElementsDataCommand).FullName);
			
var button = panel.AddItem(buttonData) as PushButton;
button.ToolTip = "Click to show data of elements of inserted number and selected type.";

var buttonContextualHelp = new ContextualHelp(ContextualHelpType.Url, "https://revitmastery.com/revit-plugin-types/");
button.SetContextualHelp(buttonContextualHelp);	
You can also add a button image, but it requires some preparation.

Images (tricky in Revit 2025)

To add a png/jpg image to controls (Buttons, TextBoxes, ComboBoxes) in Revit 2025, you need the BitmapImage class from System.Windows.Media.Imaging namespace. However, this namespace is not available by default in the .NET Class Library project and must be enabled in the project’s .csproj file:
➡️
Open [YourProjectName].csproj file (Double-click your project name in the Visual Studio Solution Explorer).
➡️
Change <TargetFramework>net8.0</TargetFramework> to <TargetFramework>net8.0-windows</TargetFramework>.
➡️
Add <UseWPF>true</UseWPF>.
C#
<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>net8.0-windows</TargetFramework>
		<UseWPF>true</UseWPF>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>

	</PropertyGroup>

	<ItemGroup>
		<Reference Include="RevitAPI">
			<HintPath>..\ExternalLibraries\RevitAPI.dll</HintPath>
			<Private>True</Private>
		</Reference>
		<Reference Include="RevitAPIUI">
			<HintPath>..\ExternalLibraries\RevitAPIUI.dll</HintPath>
			<Private>True</Private>
		</Reference>
	</ItemGroup>

</Project>
Now, you can use the BitmapImage class to add an image. For a button’s LargeImage, the image size should be 32×32 px.
C#
using System.Windows.Media.Imaging;

// [...] previous code

public Result OnStartup(UIControlledApplication application)
{
	// [...] previous code

  var buttonImageUri = new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "description_32.png")); // Image size: 32x32 px	
  var buttonImage = new BitmapImage(buttonImageUri);
  button.LargeImage = buttonImage;	

	return Result.Succeeded;
}	

When adding images to the solution/project, set “Copy to Output Directory” in the Properties to “Copy if newer” or “Copy always“.

TextBoxes

TextBoxes allow users to insert text.
Creating a TextBox is similar to creating a button. First, create a TextBoxData. Its constructor requires only one argument: name, which serves as the TextBox ID, must be unique, and is not visible in the Revit menu.
Add TextBoxData to the panel to create the TextBox. You can then attach prompt text (hint text shown in an empty TextBox), a tooltip, or an image (size 16×16 px).
C#
public Result OnStartup(UIControlledApplication application)
{
	// [...] previous code

	// TextBox
	var textBoxData = new TextBoxData("nameTextBox");
	var textBox = panel.AddItem(textBoxData) as TextBox;

	textBox.PromptText = "Number of elements";
	textBox.ToolTip = "Enter the number of elements to show and click Enter.";

	var textBoxImageUri = new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "numbers_16.png")); // Image size: 16x16 px	
	var textBoxImage = new BitmapImage(textBoxImageUri);
	textBox.Image = textBoxImage;
	
	textBox.EnterPressed += TextBox_EnterPressed; // event	

	return Result.Succeeded;
}

You must press Enter after inserting the textbox value in Revit, or the value will be cleared.

Getting TextBoxes values with Events

The purpose of the textbox is to save the inserted value and/or perform some action when the value is inserted. For that, use the EnterPressed event.

C# events trigger methods when specific actions occur, like pressing Enter. Multiple methods can be attached to a single event, but each method must meet the event’s argument requirements.
While you can create custom events, attaching methods to existing ones is typically all you need for Revit programming.

The exemplary method attached to the event will retrieve the inserted value, convert it to an integer, and display a TaskDialog. It will also persist the value for further use by using a static property of the Application class.
C#
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class RevitMasteryApplication : IExternalApplication
{
  public static int SelectedElementsNumber { get; private set; } = 1;

  public Result OnStartup(UIControlledApplication application)
  {
  	// [...] previous code
  
  	// TextBox
  	var textBoxData = new TextBoxData("nameTextBox");
  	var textBox = panel.AddItem(textBoxData) as TextBox;
  
  	textBox.PromptText = "Number of elements";
  	textBox.ToolTip = "Enter the number of elements to show and click Enter.";
  
  	var textBoxImageUri = new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "numbers_16.png")); // Image size: 16x16 px	
  	var textBoxImage = new BitmapImage(textBoxImageUri);
  	textBox.Image = textBoxImage;
  	
  	textBox.EnterPressed += TextBox_EnterPressed; // event	
  
  	return Result.Succeeded;
  }
  
  private void TextBox_EnterPressed(object? sender, Autodesk.Revit.UI.Events.TextBoxEnterPressedEventArgs e)
  {
  	var textBox = sender as TextBox;
  	var textBoxValue = textBox.Value.ToString();
  
  	var isTextBoxValueInt = int.TryParse(textBoxValue, out var intTextBoxValue);
  
  	if(isTextBoxValueInt)
  	{
  		SelectedElementsNumber = intTextBoxValue; // assigning to a static property
  		TaskDialog.Show("TextBox", $"Current texBox value is: {textBoxValue}");
  	}
  	else
  	{
  		TaskDialog.Show("TextBox", $"Insert an integer!");
  	}	
  }
}

Static properties and classes are often not recommended by guides.
However, static elements are not always harmful. For Revit programming, they can simplify actions such as persisting values.

ComboBoxes

ComboBoxes allow users to select from predefined values.
Add ComboBoxData to the panel to create the ComboBox. The ComboBox must be populated with ComboBoxMembers. Each ComboBoxMemberData is defined by a name (ID-like, not visible in Revit, and unique) and the text shown in the Revit menu.
You can retrieve the ComboBox value with the CurrentChanged event.
C#
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class RevitMasteryApplication : IExternalApplication
{
  public static Type SelectedElementType { get; private set; } = typeof(Wall);

  public Result OnStartup(UIControlledApplication application)
  {
  	// [...] previous code
  
  	// ComboBox
    var comboBoxData = new ComboBoxData("elementTypeComboBox");
    var comboBox = panel.AddItem(comboBoxData) as ComboBox;
    comboBox.ToolTip = "Select elements type.";			
    
    var comboBoxImageUri = new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "list_16.png")); // Image size: 16x16 px	
    var comboBoxImage = new BitmapImage(comboBoxImageUri);
    comboBox.Image = comboBoxImage;
    
    var comboBoxMemberData_wall = new ComboBoxMemberData("elementTypeComboBox_wall", "Walls");
    var comboBoxMember_wall = comboBox.AddItem(comboBoxMemberData_wall);
    
    var comboBoxMemberData_floor = new ComboBoxMemberData("elementTypeComboBox_floor", "Floors");
    var comboBoxMember_floor = comboBox.AddItem(comboBoxMemberData_floor);
    
    comboBox.CurrentChanged += ComboBox_CurrentChanged; //event 
  
  	return Result.Succeeded;
  }
  
    private void ComboBox_CurrentChanged(object? sender, Autodesk.Revit.UI.Events.ComboBoxCurrentChangedEventArgs e)
  {
  	var comboBox = sender as ComboBox;
  	var comboBoxValue = comboBox.Current.ItemText;
  
  	var elementType =  comboBoxValue == "Walls" ? typeof(Wall) : typeof(Floor);
  	SelectedElementType = elementType; // assigning to a static property
  
  	TaskDialog.Show("ComboBox", $"Current comboBox value is: {comboBoxValue}");
  }  	
}

Putting it all into action – Command

To bring everything together, let’s modify the Command class to utilize the persisted values from the TextBox and the ComboBox.
The command method will show a TaskDialog with names of elements of the selected type (walls or floors) existing in the project. The number of names displayed will be limited by the number inserted in the TextBox (or the number of existing elements, if smaller).
C#
using Autodesk.Revit.Attributes;
using Autodesk.Revit.UI;
using Autodesk.Revit.DB;

namespace RevitMastery.Revit
{
	[Transaction(TransactionMode.Manual)]
	[Regeneration(RegenerationOption.Manual)]
	public class ShowElementsDataCommand : IExternalCommand
	{
		public Result Execute(ExternalCommandData commandData, ref string message,
		ElementSet elements)
		{
			UIApplication uiApp = commandData.Application;
			Document doc = uiApp.ActiveUIDocument.Document;

			var elementsNumber = RevitMasteryApplication.SelectedElementsNumber;
			var elementsType = RevitMasteryApplication.SelectedElementType;

			var collector = new FilteredElementCollector(doc).WhereElementIsNotElementType().OfClass(elementsType);
			var collectedElements = collector.ToElements();

			var numberToShow = Math.Min(collectedElements.Count, elementsNumber);

			var elementsMessage = "Elements names: \n";
			for ( var i = 0; i<numberToShow; i++)
			{
				var element = collectedElements[i];
				var elementName = element.Name;
				elementsMessage += $"{i+1}. {elementName}\n";
			}			
			
			TaskDialog.Show("Show elements data", elementsMessage);

			return Result.Succeeded;
		}
	}
}

The Final Code

C#
using System.IO;
using System.Reflection;
using System.Windows.Media.Imaging;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;

namespace RevitMastery.Revit
{
	[Transaction(TransactionMode.Manual)]
	[Regeneration(RegenerationOption.Manual)]
	public class RevitMasteryApplication : IExternalApplication
	{
		public static int SelectedElementsNumber { get; private set; } = 1;
		public static Type SelectedElementType { get; private set; } = typeof(Wall);


		public Result OnStartup(UIControlledApplication application)
		{
			var ribbonTabName = "Revit Mastery";
			application.CreateRibbonTab(ribbonTabName);
			var panel = application.CreateRibbonPanel(ribbonTabName, "Revit Mastery Panel");

			var assemblyPath = Assembly.GetExecutingAssembly().Location; //localization of the currently executing assembly. Don't worry, copy ;)
			var assemblyDirectory = Path.GetDirectoryName(assemblyPath);

			// Button
			var buttonData = new PushButtonData(
				typeof(ShowElementsDataCommand).FullName,
				"Show elements data", 
				assemblyPath,
				typeof(ShowElementsDataCommand).FullName);
			
			var button = panel.AddItem(buttonData) as PushButton;
			button.ToolTip = "Click to show data of elements of inserted number and selected type.";

			var buttonContextualHelp = new ContextualHelp(ContextualHelpType.Url, "https://revitmastery.com/revit-plugin-types/");
			button.SetContextualHelp(buttonContextualHelp);

			var buttonImageUri = new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "description_32.png")); // Image size: 32x32 px	
			var buttonImage = new BitmapImage(buttonImageUri);
			button.LargeImage = buttonImage;


			// TextBox
			var textBoxData = new TextBoxData("nameTextBox");
			var textBox = panel.AddItem(textBoxData) as TextBox;

			textBox.PromptText = "Number of elements";
			textBox.ToolTip = "Enter the number of elements to show and click Enter.";

			var textBoxImageUri = new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "numbers_16.png")); // Image size: 16x16 px	
			var textBoxImage = new BitmapImage(textBoxImageUri);
			textBox.Image = textBoxImage;
			
			textBox.EnterPressed += TextBox_EnterPressed; // event
			

			// ComboBox
			var comboBoxData = new ComboBoxData("elementTypeComboBox");
			var comboBox = panel.AddItem(comboBoxData) as ComboBox;
			comboBox.ToolTip = "Select elements type.";			

			var comboBoxImageUri = new Uri(Path.Combine(assemblyDirectory, "Resources", "Images", "list_16.png")); // Image size: 16x16 px	
			var comboBoxImage = new BitmapImage(comboBoxImageUri);
			comboBox.Image = comboBoxImage;

			var comboBoxMemberData_wall = new ComboBoxMemberData("elementTypeComboBox_wall", "Walls");
			var comboBoxMember_wall = comboBox.AddItem(comboBoxMemberData_wall);

			var comboBoxMemberData_floor = new ComboBoxMemberData("elementTypeComboBox_floor", "Floors");
			var comboBoxMember_floor = comboBox.AddItem(comboBoxMemberData_floor);

			comboBox.CurrentChanged += ComboBox_CurrentChanged; //event 
			

			return Result.Succeeded;
		}			

		private void TextBox_EnterPressed(object? sender, Autodesk.Revit.UI.Events.TextBoxEnterPressedEventArgs e)
		{
			var textBox = sender as TextBox;
			var textBoxValue = textBox.Value.ToString();

			var isTextBoxValueInt = int.TryParse(textBoxValue, out var intTextBoxValue);

			if(isTextBoxValueInt)
			{
				SelectedElementsNumber = intTextBoxValue; // assigning to a static property
				TaskDialog.Show("TextBox", $"Current texBox value is: {textBoxValue}");
			}
			else
			{
				TaskDialog.Show("TextBox", $"Insert an integer!");
			}			
		}

		private void ComboBox_CurrentChanged(object? sender, Autodesk.Revit.UI.Events.ComboBoxCurrentChangedEventArgs e)
		{
			var comboBox = sender as ComboBox;
			var comboBoxValue = comboBox.Current.ItemText;

			var elementType =  comboBoxValue == "Walls" ? typeof(Wall) : typeof(Floor);
			SelectedElementType = elementType; // assigning to a static property

			TaskDialog.Show("ComboBox", $"Current comboBox value is: {comboBoxValue}");
		}


		public Result OnShutdown(UIControlledApplication application)
		{
			return Result.Succeeded;
		}
	}
}
C#
using Autodesk.Revit.Attributes;
using Autodesk.Revit.UI;
using Autodesk.Revit.DB;

namespace RevitMastery.Revit
{
	[Transaction(TransactionMode.Manual)]
	[Regeneration(RegenerationOption.Manual)]
	public class ShowElementsDataCommand : IExternalCommand
	{
		public Result Execute(ExternalCommandData commandData, ref string message,
		ElementSet elements)
		{
			UIApplication uiApp = commandData.Application;
			Document doc = uiApp.ActiveUIDocument.Document;

			var elementsNumber = RevitMasteryApplication.SelectedElementsNumber;
			var elementsType = RevitMasteryApplication.SelectedElementType;

			var collector = new FilteredElementCollector(doc).WhereElementIsNotElementType().OfClass(elementsType);
			var collectedElements = collector.ToElements();

			var numberToShow = Math.Min(collectedElements.Count, elementsNumber);

			var elementsMessage = "Elements names: \n";
			for ( var i = 0; i<numberToShow; i++)
			{
				var element = collectedElements[i];
				var elementName = element.Name;
				elementsMessage += $"{i+1}. {elementName}\n";
			}			
			
			TaskDialog.Show("Show elements data", elementsMessage);

			return Result.Succeeded;
		}
	}
}