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

Creating a Revit plugin allows you to modify the Revit menu by adding your own panels, tabs, buttons, and other controls. You can also customize them with images, tooltips, or extended contextual help. However, especially in the new Revit 2025 .NET API, there are some caveats.

Where to start – IExternalApplication.OnStartup

To modify the Revit menu, you need to create an Application plugin type.
To create the Application plugin, implement the IExternalApplication interface with its OnStartup and OnShutdown methods. The code inside the OnStartup method is executed during Revit’s opening, so this is where to insert the menu-modifying code.
To get along with the whole plugin creation process, check the Start Revit API 2025 c# Programming Guide.
The code below creates a ribbon tab called Revit Mastery and a ribbon panel called Revit Mastery 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;
		}
	}
}

Buttons: creating and customizing

Next, we can add controls to our panel. 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. This is the same for each control, so you can simply copy it as boilerplate code.
className: link to a command class that will be executed when the button is clicked.
So actually, we need two variables to define a button: text to display and the name of a command class to execute on click.
The command class associated with the button must implement the IExternalCommand interface. When the button is clicked, the Execute method will be invoked.
For now, we will just notify the user that the button has been clicked.
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)
		{				
			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.
You can also add a ToolTip and ContextualHelp. The ContextualHelp can, for example, link to a website, which will appear when the user clicks F1 while hovering over the button.
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");

			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);			

			return Result.Succeeded;
		}
		
		public Result OnShutdown(UIControlledApplication application)
		{
			return Result.Succeeded;
		}
	}
}
You can also add a button image, but it requires some preparation.

Images (tricky in Revit 2025)

In Revit 2025’s .NET API, adding images to controls (buttons, textBoxes, comboBoxes) requires some tweaks in the project configuration.
To add a png/jpg image to a control, we need the BitmapImage class from System.Windows.Media.Imaging class 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.
Here’s a step-by-step process:
Double-click your project name in the Visual Studio Solution Explorer to open [YourProjectName].csproj project.
Change <TargetFramework>net8.0</TargetFramework> to <TargetFramework>net8.0-windows</TargetFramework>.
Add <UseWPF>true</UseWPF>.
Save the file.
Finally, the .csproj file code should be similar to:
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 System.Windows.Media.Imaging.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 object. The TextBoxData constructor requires only one argument: name. This name, like the PushButtonData, 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, we need to use a textbox event EnterPressed.

If you are unfamiliar with C# Events, they work like this: we attach a method that will be fired when an event (e.g., Enter pressed) occurs. One event can have multiple methods attached. The attached methods must have specific arguments, following the events requirements.
You can create your own events, but attaching methods to existing events is usually sufficient for Revit programming.

Our method attached to the event will retrieve the inserted value and try to convert it to an integer. Then, it will display a TaskDialog informing about the value.

The method will also persist the value for further use by other methods (e.g. when clicking a button). We will do it with a simple approach: 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.
Indeed, there are cases when static elements should be avoided such as when an app can be used simultaneously by multiple users, like in web apps. However, static elements are not harmful when you know what you are doing and what kind of application you are creating. For Revit programming, they can be really useful in simplifying actions such as persisting values.

ComboBoxes

ComboBoxes allow users to select from predefined values.
Creating a ComboBox is similar to creating a TextBox. Add ComboBoxData to the panel to create the ComboBox. Additionally, 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.
Similar to a TextBox, you can retrieve the ComboBox value with an event – in this case, CurrentChanged.
Our method attached to this event will display a TaskDialog with the current value, determine the element type, and save the type to a static property of the Application class for later use.
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;
		}
	}
}

Summary

Creating Revit menu tabs, panels, and controls is one of the first steps toward developing powerful plugins for you and your clients. Customizing the controls adds a “styling touch.” While buttons are the most popular control types, TextBoxes and ComboBoxes are also useful to save values and extend plugins’ capabilities.
Do you want to become the Revit Programmer of the Future?