Microsoft Access applications often live or die by their user experience. One of the most effective ways to control that experience is through a custom Ribbon. When designed correctly, a ribbon can guide users, reduce mistakes, and make an Access database feel like a purpose-built application instead of a collection of forms.
This article walks through the entire process of creating a custom ribbon in Access from how ribbons are stored, to debugging XML, to building dynamic, role-aware interfaces using VBA.
How Access Loads Custom Ribbons
Unlike other Office applications, Access does not load ribbon definitions from external files. Instead, it reads ribbon XML from a system table inside the database. When Access opens, or when a form or report becomes active, it parses that XML and constructs the Ribbon interface dynamically.
This design has several implications:
- The ribbon travels with the database file
- Ribbon syntax errors can prevent the UI from loading
- Debugging must be explicitly enabled
Creating the Ribbon Table
Access looks for ribbon definitions in a table named USysRibbons. The name and structure are not optional.
Use the following SQL to create the table:
CREATE TABLE USysRibbons
(
RibbonName TEXT(255) NOT NULL,
RibbonXML MEMO
);
Important:
- The table name must be exactly
USysRibbons - This is a system table and may be hidden by default
RibbonNameis what you select later in Access Options
Because this is a System table, it is normally hidden. As such, I typically will create a query to be able to work with it without the need of always enabling the display of System Object (and then disabling it later). So if you are so inclined, you can create a very simple query such as:
SELECT USysRibbons.* FROM USysRibbons;
and now, you’ll always have direct access via the saved query (you can give it any name you wish).
Warning About Editing Ribbon XML!
One of the most important lessons I’ve learned over the years of creating custom Ribbons in Access is to never edit the XML directly in the USysRibbon table.
Don’t ask me why, but something goes wrong every single time. I place the cursor where I want to make a change, start typing, and the text becomes scrambled—eventually corrupting the XML. There is clearly a serious bug in how Access handles XML editing in tables.
Instead, I always copy the XML out of Access and edit it in my preferred external editor (Notepad++, Sublime Text, VSCodium, etc.), then copy and paste it back into Access. Yes, this adds a few extra steps, but the process is reliable, and the editing tools are far superior.
Using external editors also makes troubleshooting much easier. They provide line numbers and character positions, allowing you to quickly locate and fix the issues identified in Access error messages.
XML Syntax
Microsoft’s Ribbon XML supports both Self-closing and Explicit tags.
Self-closing
Compact and clean for simple references that stay on one line.
<group idMso="GroupClipboard"/>
Explicit tags
Perfect for multi-line entries with children, or when you plan to add elements later.
<group idMso="GroupClipboard"></group>
Use whichever style fits your coding preference and know that both work perfectly together in the same Ribbon XML.
Commenting Your XML
When customizing the Office Ribbon using XML, it’s essential to keep the structure readable and maintainable especially when multiple elements define tabs, groups, and controls. Adding comments in your Ribbon XML helps future you (or teammates) understand what each customization does.
XML comments use the same tags/pattern as standard HTML:
<!-- Your comment goes here -->
For example, you might insert
<!-- Custom 'My Tools' tab for quick access -->
<tab id="tabMyTools" label="My Tools">
<!-- Group for frequently used actions -->
<group id="groupQuickActions" label="Quick Actions">
<button id="btnLaunchReport" label="Run Report" onAction="OnLaunchReport" /> <!-- For the Finance Dept. -->
</group>
</tab>
You can also use comments to temporarily comment out sections while testing changes, so you don’t have to delete the code entirely:
<!-- Temporarily removing this group for testing -->
<!--
<group id="groupDeprecated" label="Old Tools">
<button id="btnOldFeature" label="Legacy Function" onAction="DoOldFeature" />
</group>
-->
Using comments effectively like this makes your Ribbon XML far easier to debug, refine, and continue development at a later date.
Bare Bone Ribbon XML Template
Below is the most bare bones XML template of a custom ribbon. This permits you to see the basic structure and will permit us to start our discussion.
It starts off by defining: the Namespace to use, whether to start from scratch or not, then defines a new tab, group and button.
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui">
<ribbon startFromScratch="false">
<tabs>
<tab id="MyTab" label="My Tab">
<group id="MyGroup" label="My Group">
<button id="MyButton" label="Click Me" />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
If you review the above XML, you’ll see it is broken done in a structure:
<customUI>
└── <ribbon>
└── <tabs>
└── <tab id="MyTab" label="My Tab">
└── <group id="MyGroup" label="My Group">
└── <button id="MyButton" label="Click Me" />
- Root element of all Ribbon customizations – customUI
- Ribbon-specific customizations – ribbon
- Container for custom tabs – tabs
- Ribbon tab (e.g., “Home”, “Insert”, or your custom one) – tab
- Groups controls within a tab – group
- Individual controls (button, box, checkBox, …)
and you can customize and create as many Tab, Group and/or Control elements as you please to make your ribbon as you please.
Understanding the Two Ribbon XML Namespaces (xmlns Attribute) in Microsoft Access
When creating a custom Ribbon in Microsoft Access, you must specify an XML namespace using the xmlns attribute. Access supports two different Ribbon XML namespaces. They look similar, but they enable very different capabilities.
Choosing the correct namespace is critical, because it determines whether your ribbon can be dynamic, role-aware, and refreshable or permanently static.
The Two Available Ribbon XML Namespaces
1. Office 2007 (Legacy Namespace)
xmlns="http://schemas.microsoft.com/office/2006/01/customui"
2. Office 2010 and Later (Extended Namespace)
xmlns="http://schemas.microsoft.com/office/2009/07/customui"
Both namespaces are still supported by Access today, but they exist for historical and compatibility reasons.
Why Two Namespaces Exist
The Ribbon interface was first introduced in Office 2007. When Microsoft enhanced the Ribbon for Office 2010, new features were added that could not be introduced without breaking existing solutions.
Instead of modifying the original schema, Microsoft created a new namespace with expanded capabilities.
- 2006 namespace – Original Ribbon schema (Office 2007)
- 2009 namespace – Extended Ribbon schema (Office 2010+)
The newer namespace is a superset of the older one.
Bottom Line
The two Ribbon XML namespaces exist solely for backward compatibility. For any modern Microsoft Access development, you should always use:
xmlns="http://schemas.microsoft.com/office/2009/07/customui"
It provides greater flexibility, cleaner design patterns, and future-proof ribbon architecture.
Attributes and Callbacks
I think it is important, before diving any further into programming that we briefly clarify two common Ribbon terms: Atributes & Callbacks. In plain English:
Attributes are settings you add to ribbon buttons/groups/tabs in the XML like label=”Click Me”, enabled=”true”, or visible=”false”. You have an attribute (label, enabled, visible, …) followed by its value. They control how things look and behave. Different XML elements have different attributes, so it is important to review support documentation to know which can be defined, or not.
Callbacks are your VBA procedures that Access automatically runs when something happens like onAction=”MyClick” tells Access “run my MyClick() when this button is clicked.” In this context, onAction is the Attribute and MyClick is the Callback procedure.
It is important to understand that attributes can have a static value assigned just as much as you could assign the very same attribute a callback for more complex situations.
' Invariable - Fixed label <button id="btn1" label="Save Now" onAction="DoSave" /> ' Dynamic label via callback (vba procedure) <button id="btn1" getLabel="GetMyLabel" onAction="DoSave" />
So where can you find a listing of the available attributes? I’d start by reviewing:

Note: You will notice that even this support documentation omits information specifically regarding Access. For instance, if you want to review the idMso values for assigning images to elements, they have tables with the values for Word, Excel and PowerPoint, but not Access!
Plain Attributes vs ‘get’ Attributes
When working with the Access ribbon, you’ll notice pairs of similar attributes in the documentation, such as:
- enabled and getEnabled
- visible and getVisible
- etc.
The key difference lies in how the values are defined:
Plain attributes (without the get prefix) are used for static, hardcoded values. So, for static elements that never change.
<button visible="true" ... />
Here, visible=”true” simply sets the button to always be visible.
get attributes (with the get prefix) are used when you want the value to be determined dynamically via a callback procedure. Hence, for elements that you wish to dynamically change during the usage of your application dependent on various factors controlled within your VBA callback procedure.
<button getVisible="GetVisibility" ... />
In this example, Access will call the GetVisibility VBA function at runtime to decide whether the button should be visible.
Essentially, if your ribbon behavior is static, use the plain attributes. If it needs to respond to runtime conditions like user permissions or form states then use the ‘get’ attributes.
To startFromScratch, Or Not?
When you use
<ribbon startFromScratch="true">
in a custom Access ribbon, Access does not load any built-in tabs such as Home, Create, or External Data. For example, in a deployed Access application for end users, you might define a single custom tab called “Data Entry” with just a few buttons: Add Record, Save, and Print. Because the ribbon starts from scratch, users cannot see or access design tools, query editors, or export commands only the buttons you explicitly define are available. This is especially useful for kiosk-style databases or applications where users should not modify objects or data structure.
By contrast, when you implement
<ribbon startFromScratch="false">
OR, simply ommit the startFromScratch attribute altogether like
<ribbon>
Access loads the default ribbon first and then applies your custom ribbon. For example, you might add a new “Reporting Tools” tab that includes buttons for Open Monthly Report and Export to PDF, while leaving the built-in Home and Create tabs intact. Alternatively, you could hide specific built-in tabs (such as Create) while keeping others visible. This approach is ideal when power users still need standard Access functionality, but you want to streamline or extend the interface with custom application-specific commands.
Database Ribbon vs Form/Report Ribbon
Database Level Ribbon
A database ribbon is assigned in File -> Options -> Current Database. It loads when the application opens and acts as the default UI.
Use database ribbons for:
- Application navigation
- Global commands
- Locking down the default Access interface
Form and Report Ribbons
Forms and reports can each specify their own ribbon using the Ribbon Name property (available under the Other tab in the form’s Property Sheet). When the object is active, its ribbon temporarily replaces the database ribbon.
Use form/report ribbons for:
- Record lifecycle actions
- Workflow-specific tools
- Context-sensitive commands
This layered approach reduces logic complexity and results in a cleaner user experience.
Enabling Ribbon Debugging
By default, Access fails silently when ribbon XML is invalid. To see meaningful errors during development you must enable error reporting by doing:
- Open File -> Options
- Select Client Settings
- Enable Show add-in user interface errors
Once enabled, Access will display XML parsing and callback errors instead of hiding them.
Capturing the Ribbon Object
One of the most important steps we must take to be able to actually automate the Ribbon in any manner is to initially get a reference to it! To do so is very simple. All we need to do is define a callback that will be used/run when the Ribbon loads. This is done by defining the customUI onLoad attribute to a callback which will assign the ribbon object to a variable for future use:
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoad">
Then in our database, we will define a globabl Ribbon variable and the OnRibbonLoad callback procedure. Basically, by setting the attribute to a procedure, it will run the procedure automatically when the Ribbon is loaded. Thus, in its simplest form, we will end up with something along the lines of:
Public oRibbonUI As IRibbonUI ' Our ribbon variable for future use!
Public Sub OnRibbonLoad(ribbon As IRibbonUI)
' Get a reference to the Ribbon object and assign it to our variable
Set oRibbonUI = ribbon
End Sub
Now that we have a reference to the Ribbon, we are ready to move forward and start to automate it!
Refreshing the Ribbon
When application state changes, we can force Access to re-evaluate its callbacks:
Public Sub RibbonRefresh
If Not oRibbonUI Is Nothing Then oRibbonUI.Invalidate
End Sub
This could be required after role changes, mode switches, or form navigation…
It is also possible to refresh inidividual elements, such as command buttons. This would be done by doing:
Public Sub RibbonRefreshElement(ByVal sYourElementId As String)
If Not oRibbonUI Is Nothing Then
oRibbonUI.InvalidateControl sYourElementId
End If
End Sub
and you can then use it by simply calling it like:
Call RibbonRefreshElement("btnStatus") ' Refresh a button
By invalidating the Ribbon, or element, you force Access (in this case) to re-evaluate callbacks like getLabel, getEnabled, …
Obviously, it is far more efficient to invalidate an individual element using RibbonRefreshElement rather than the entire Ribbon using RibbonRefresh. So use your judgement as to which procedure to run to optimize performance.
visible/getVisible Attribute
The visible attribute controls whether an element is displayed at all. When an element is set to not visible (false), it is completely removed from the interface, and the space it would normally occupy is taken up by the next visible element. No blank space is left behind.
enabled/getEnabled Attribute
In contrast, the enabled attribute keeps the element visible but makes it inactive and inaccessible to the user. Disabled elements (false) appear greyed out and cannot be interacted with. In most cases, I prefer using the enabled attribute rather than changing visibility, as it preserves the layout and provides clearer feedback to the user.
Callback Design Best Practices
Whatever you do, avoid placing your actual application logic directly inside ribbon callbacks. Callbacks are part of the UI layer and should not be responsible for executing business logic or handling complex error conditions.
Public Sub SaveRecord(oControl As IRibbonControl)
On Error GoTo Error_Handler
If Not Screen.ActiveForm Is Nothing Then
If Screen.ActiveForm.Dirty Then
DoCmd.RunCommand acCmdSaveRecord
End If
End If
Error_Handler_Exit:
On Error Resume Next
Exit Sub
Error_Handler:
MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
"Error Source: SaveRecord" & vbCrLf & _
"Error Number: " & Err.Number & vbCrLf & _
"Error Description: " & Err.Description & _
Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
, vbOKOnly + vbCritical, "An Error has Occurred!"
Resume Error_Handler_Exit
End Sub
Whatever you do, avoid placing your actual application logic directly inside ribbon callbacks. Callbacks are part of the UI layer and should not be responsible for executing business logic or handling complex error conditions.
Instead, ribbon callbacks should act as simple delegates. Their role is simply to respond to user interaction and pass control to an appropriate procedure and nothing more:
Public Sub SaveRecord(oControl As IRibbonControl)
Call SaveActiveForm
End Sub
Here, the callback merely forwards the request. The actual work is performed elsewhere, keeping the ribbon layer focused on UI concerns and avoiding duplication or tight coupling between the ribbon and application logic.
Following this pattern results in cleaner, more maintainable code and makes it easier to modify or extend application behavior without revisiting ribbon callbacks.
A Single Callback To Rule Them All
One approach is to create a dedicated callback for each ribbon element:
Public Sub SaveRecord(oControl As IRibbonControl)
Call SaveActiveRecord
End Sub
Public Sub DeleteRecord(oControl As IRibbonControl)
Call DeleteActiveRecord
End Sub
...
Personally, I prefer using a single callback to handle delegation for related elements, dispatching behavior based on the control’s ID:
Sub GetElementAction(oControl As IRibbonControl)
Select Case oControl.Id
Case "FormSaveRecord"
Call SaveActiveRecord
Case "FormDeleteRecord"
Call DeleteActiveRecord
End Select
End Sub
This approach centralizes ribbon interaction in one place, making callbacks easier to locate and reducing the overall amount of ribbon-related code. As long as the dispatcher is limited to routing requests and does not accumulate business logic, the ribbon layer remains clean and maintainable.
A Production-Ready Ribbon XML Template
Below is an example of a structured ribbon designed for real applications. It separates navigation, record actions, and administrative tools while supporting dynamic behavior.
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoad">
<ribbon startFromScratch="true">
<tabs>
<tab id="tabApp" label="Application">
<group id="grpNav" label="Navigation">
<button id="btnHome"
label="Home"
imageMso="GoToHome"
size="large"
onAction="GoHome"/>
</group>
<group id="grpRecords" label="Records">
<button id="btnNew"
label="New"
imageMso="NewRecord"
onAction="NewRecord"
getEnabled="CanEdit"/>
<button id="btnSave"
label="Save"
imageMso="Save"
onAction="SaveRecord"
getEnabled="CanEdit"/>
</group>
<group id="grpAdmin" label="Administration"
getVisible="IsAdmin">
<button id="btnUsers"
label="Users"
imageMso="GroupInsert"
onAction="ManageUsers"/>
</group>
</tab>
</tabs>
</ribbon>
</customUI>
Notice the structure and how each element is given a unique and meaningful id which we can later use in our code for all sorts of fun things.
Using Built-In Tabs/Groups/Elements
One of the marvelous aspects of Ribbon development is that Microsoft enables us to utilize the built-in Tabs, Groups and/or individual Elements. So you can retain what you want, or customize your own Tab with built-in utilities with relative ease.
Implementation Examples
It all comes down to knowing and using the Element’s idMso, it internal name, and using that instead of assigning it an id. Let me show you an example of each.
Tab
<tab idMso="TabDatabaseTools" visible="true"></tab>
Group
<group idMso="GroupClipboard"></group>
Element
<button idMso="ExportExcel" label="Export to Excel" size="normal"/>
Omitting a Group From a Built-In Tab
It is easy to further control which groups are visible and/or enabled in any built-in toolbar by simply including an entry for that group idMso and setting its enabled or visible attribute to false.
In this example I’m displaying the built-in DATABASE TOOLS tab, but hiding the Move Data group.
<tab idMso="TabDatabaseTools">
<group idMso="GroupMoveData" visible="false" ></group>
</tab>
Omitting an Element From a Built-In Group
One thing that I’ve never quite understood is that if you wish to hide an individual element from a built-in group, it is not possible, you’ll receive an error 0x80004005: Controls in a built-in group cannot be modified.
The way I handle this is to recreate the group myself and add just the individual control idMsos I want, omitting the one(s) I don’t.
For example, say I want to display the DATABASE TOOLS tab but omit the Move Data group altogether and omit the Run Macro button within the Macro Group, my Ribbon XML would look like:
<tab idMso="TabDatabaseTools" visible="true">
<group idMso="GroupMoveData" visible="false" ></group> <!-- Hide Move Data group -->
<group idMso="GroupMacro" visible="false"></group> <!-- Hide the original Macro group -->
<group id="MyGroupMacro" label="Macro" insertBeforeMso="GroupViewsShowHide"> <!-- Recreate the Macro group myself -->
<button idMso="VisualBasic" size="large" label="Visual Basic"/>
</group>
</tab>
IdMso Invariability
For multilingual deployments, idMso values remain constant across all languages and they don’t localize, simplifying Ribbon development with built-in elements.
Where To Find the IdMso(s)!
I’m sure by now you’re asking yourself how can I identify the idMso of all the Ribbon elements! Sadly, Microsoft did not expose them for us to be able to use VBA, UIAutomation or some other technique to retrieve a list, but they have supplied downloadable listing available on their website.
It is also important to understand that idMso have evolved with the changing Office versions, so you need to download and refer to the documentation that matches your Office installation. Several of the original links no longer work so I’ve pointed you towards the Web Archive backup copies instead.
Images
I wrote a post about Missing Ribbon Images back in 2022 about this very issue!
Final Thoughts
A well-designed Access ribbon is not just cosmetic. It is a state-aware UI layer that guides users, enforces workflow, and simplifies complex applications.
When you understand that:
- XML defines structure
- VBA defines behavior
- Invalidate controls state
- Form ribbons reduce complexity
…custom ribbons become one of the most powerful tools in the Access platform.
I’m hoping this tutorial will help quickly bring up to speed novice ribbon developers and make key concepts easy to digest and give you the confidence to create your own.
I may do a part 2 where I’d go over more advance concepts such as: ordering/positioning element, using images (bmp, ico, …) as element images, controlling what is enabled based on the current user/group, …
Page History
| Date | Summary of Changes |
|---|---|
| 2026-01-25 | Initial Release – Work In Progress! |