Excel UserForm in VBA: How to Create, Show, Load and Close One, With Working Code

Tested in Excel. On 24 October 2026 we built this guide's form in Excel for Microsoft 365 on Windows, pasted in every code block exactly as published, compiled it with no errors and used it: picked an invoice, typed an invalid amount, saved a correction, closed with the X and reopened. Everything behaved as described. Our copy of Excel is the 32-bit build; the code makes no API calls, so the bitness makes no difference. The screenshots are from that test (select one to see it full size).

An Excel UserForm is a custom dialog box you build in the VBA editor to collect and check input. This guide builds an invoice picker that corrects table rows, and covers Load versus Show, events, tab order, closing cleanly, and 64-bit, Mac and browser limits.

Last checked: 24 October 2026. Code targets 64-bit Microsoft 365 Excel for Windows. To add new rows with full validation, see how to create a data entry form in Excel, which uses the same register.

How to create a UserForm in Excel VBA: the short version

  1. Save as .xlsm and press Alt+F11 to open the Visual Basic Editor.
  2. Choose Insert > UserForm and set its (Name) and Caption in the Properties window (F4).
  3. Drag controls from the Toolbox and name each one, such as txtNet.
  4. Right-click the form, choose View Code, and fill lists and defaults in UserForm_Initialize.
  5. Write Click code for the buttons: check entries, write to the table, close.
  6. In a standard module, add a macro that shows the form and attach it to a sheet button.

Microsoft's Create a User Form page covers step 2. The Developer tab is needed only for the sheet button in step 6 (show the Developer tab).

What is an Excel UserForm?

A UserForm is a window or dialog box that forms part of your macro's interface (UserForm object). Its code module responds to events like a click. A form earns its place when people enter or correct records away from the sheet; if a table with a few drop-down lists does the job, skip it.

UserForm controls in the Toolbox

The Toolbox offers the 14 standard Microsoft Forms controls below, each with an ID such as Forms.TextBox.1 (Add method).

ControlTypical finance usePrefix
LabelCaptions, status messageslbl
TextBoxInvoice number, amounttxt
ComboBoxSupplier, VAT ratecbo
ListBoxList of invoiceslst
CheckBoxVAT included, paidchk
OptionButtonDebit or creditopt
ToggleButtonAn on/off switchtgl
FrameGrouping related controlsfra
CommandButtonSave, Closebtn
MultiPageHeader and lines pagesmpg
TabStripTabs over shared controlstab
SpinButtonStepping a period numberspn
ScrollBarScrolling through valuesscr
ImageA company logoimg

The example form used in this guide

Every snippet assumes these names:

UserForm design basics: tab order, alignment and keyboard access

Make the form work from the keyboard:

How to show a UserForm in Excel VBA

Call the form's Show method from a macro in a standard module (Insert > Module). If the form isn't loaded yet, VBA loads it first (Show method).

Option Explicit

Public Sub OpenInvoiceEditSimple()
    frmInvoiceEdit.Show
End Sub

This uses the form's default instance, fine for a quick test. For a sheet button, choose Developer > Insert > Button (Form Control), draw it and assign the macro. A UserForm can't sit on a sheet itself.

How to load, show, hide and unload a UserForm

Use Load to put a form in memory, Show to display (and if needed load) it, Hide to take it off screen while keeping its values, and Unload to remove it.

StatementWhat happensEvents that runValues afterwards
Load frmInvoiceEditIn memory, not visible (Load)InitializeFresh defaults
frmInvoiceEdit.ShowLoads if needed, then displaysInitialize (if not loaded), ActivateAs set by Initialize
Me.HideOff screen, still loaded (Hide)No load or close eventsKept, readable by code
Unload MeRemoved from memory (Unload)QueryClose; Terminate once nothing refers to itGone

The classic trap: a hidden form keeps its old entries when shown again, because Initialize doesn't re-run. Hide only when the calling macro still needs the values.

How to initialize a UserForm in VBA

Put setup code in UserForm_Initialize, which runs after the form loads and before it appears (Initialize event). This goes at the top of the form's code module:

Option Explicit

Private m_Row As Long          ' table row being edited (1 = first data row, 0 = none)
Private m_SavedCount As Long

Public Property Get SavedCount() As Long
    SavedCount = m_SavedCount
End Property

Private Sub UserForm_Initialize()
    Dim lo As ListObject
    Set lo = ThisWorkbook.Worksheets("Lists").ListObjects("tblSupplierList")
    Select Case lo.ListRows.Count
        Case 0
            ' no suppliers yet: leave the list empty
        Case 1
            Me.cboSupplier.AddItem CStr(lo.DataBodyRange.Cells(1, 1).Value)
        Case Else
            Me.cboSupplier.List = lo.ListColumns(1).DataBodyRange.Value
    End Select
    Me.txtInvoiceNo.Locked = True  ' the invoice number identifies the row
    LoadInvoiceList
End Sub

Assigning a column to .List loads it in one step (List property); a single cell isn't an array, hence AddItem. A locked box can't be edited. SavedCount tells the calling macro what happened.

Initialize runs once per load, unlike Activate. Don't call Me.Show inside Initialize: the form appears before setup finishes, and a modal form pauses the rest of Initialize until it closes.

Filling a multi-column ListBox

Set ColumnCount and assign a block of cells. The first four table columns are Date, Supplier, Invoice No and Net:

Private Sub LoadInvoiceList()
    Dim lo As ListObject
    Set lo = ThisWorkbook.Worksheets("Register").ListObjects("tblPurchaseRegister")
    m_Row = 0
    Me.btnSave.Enabled = False     ' nothing to save until a row is picked
    Me.lstInvoices.Clear
    If lo.ListRows.Count = 0 Then Exit Sub
    With Me.lstInvoices
        .ColumnCount = 4
        .ColumnWidths = "70 pt;140 pt;80 pt;70 pt"
        .List = lo.DataBodyRange.Resize(, 4).Value
    End With
End Sub

List rows and columns count from zero, so Me.lstInvoices.List(Me.lstInvoices.ListIndex, 2) is the selected invoice number. ListIndex is -1 when nothing is selected (ListIndex).

Editing the selected record

Selecting a row runs the ListBox's Click event (Click event). The list was loaded straight from the table, so list row 0 is table row 1:

Private Sub lstInvoices_Click()
    Dim lo As ListObject
    Dim r As Range
    If Me.lstInvoices.ListIndex = -1 Then Exit Sub

    Set lo = ThisWorkbook.Worksheets("Register").ListObjects("tblPurchaseRegister")
    m_Row = Me.lstInvoices.ListIndex + 1
    Set r = lo.ListRows(m_Row).Range
    Me.txtInvoiceNo.Value = CStr(r.Cells(1, lo.ListColumns("Invoice No").Index).Value)
    Me.txtDate.Value = CStr(r.Cells(1, lo.ListColumns("Date").Index).Value)
    SelectSupplier CStr(r.Cells(1, lo.ListColumns("Supplier").Index).Value)
    Me.txtNet.Value = CStr(r.Cells(1, lo.ListColumns("Net").Index).Value)
    Me.txtVAT.Value = CStr(r.Cells(1, lo.ListColumns("VAT").Index).Value)
    Me.btnSave.Enabled = True
End Sub

Private Sub SelectSupplier(ByVal supplierName As String)
    Dim i As Long
    Me.cboSupplier.ListIndex = -1
    For i = 0 To Me.cboSupplier.ListCount - 1
        If StrComp(Me.cboSupplier.List(i, 0), supplierName, vbTextCompare) = 0 Then
            Me.cboSupplier.ListIndex = i
            Exit For
        End If
    Next i
End Sub

If a supplier has since left tblSupplierList, SelectSupplier leaves the box empty for the user to pick again. Because the form is modal, nobody can sort the table meanwhile.

Modal vs modeless UserForms

A modal form (the default) blocks the rest of Excel and pauses the calling macro until it is hidden or unloaded. A modeless form lets people keep working.

QuestionModal (vbModal, default)Modeless (vbModeless)
How to set itfrm.Show or frm.Show vbModalfrm.Show vbModeless, or ShowModal = False in the Properties window (read-only at run time)
Does the calling macro wait?Yes, until hidden or unloadedNo, code after Show runs at once
Good forData entry, editing recordsA lookup panel beside a ledger
Watch out forNothing after Show runs until it closesNo taskbar entry; data can be lost if you edit the project so it recompiles (for example, by removing a module); must save its own work

Sources: Show method, ShowModal property.

UserForm events: which one to use when

Typical order: Initialize, Activate, control events, QueryClose, Terminate.

EventWhen it runsUse it to
UserForm_InitializeAfter loading, before showingFill lists, set defaults
UserForm_ActivateWhen the visible form becomes activeRefresh totals
lstInvoices_ClickWhen the user selects a rowLoad the record
btnSave_ClickWhen clicked, or Enter on the Default buttonValidate and save
txtNet_BeforeUpdateBefore changed data is committed, ahead of AfterUpdate and ExitField checks
UserForm_QueryCloseBefore the form closes, whatever the causeHandle the X button
UserForm_TerminateAfter unloadingFinal clean-up

Sources: BeforeUpdate, QueryClose, Terminate. End stops Terminate from running, so close forms with Unload, never End.

Events for controls added in code: the WithEvents wiring

VBA links a handler such as btnSave_Click automatically only for controls placed in the designer. A control created with Me.Controls.Add needs a WithEvents variable:

Private WithEvents m_btnExport As MSForms.CommandButton

Private Sub UserForm_Initialize()
    Set m_btnExport = Me.Controls.Add("Forms.CommandButton.1", "btnExport")
    With m_btnExport
        .Caption = "Export"
        .Left = 12: .Top = 180: .Width = 72: .Height = 24
    End With
End Sub

Private Sub m_btnExport_Click()
    MsgBox "Export clicked", vbInformation
End Sub

All three parts must exist: the module-level WithEvents variable, the Set, and a handler named variable_Event. Miss one and the button does nothing. WithEvents works only in class modules (form modules count), not with New and not as an array (Dim statement).

In the example form, put the WithEvents line at the top of the module (declarations below a procedure won't compile) and merge the Set and With lines into the existing UserForm_Initialize.

How to validate UserForm entries before saving

Check entries when the user clicks Save: explain the first problem, put the cursor there and stop. For duplicate checks and appending rows, see building a validated data entry form.

Private Function InputIsValid() As Boolean
    If m_Row = 0 Then
        MsgBox "Pick an invoice from the list first.", vbExclamation, "Invoice edit"
    ElseIf Not IsDate(Me.txtDate.Value) Then
        ShowProblem Me.txtDate, "Enter a valid invoice date."
    ElseIf Me.cboSupplier.ListIndex = -1 Then
        ShowProblem Me.cboSupplier, "Pick a supplier from the list."
    ElseIf Not IsAmount(Me.txtNet.Value, False) Then
        ShowProblem Me.txtNet, "Enter a net amount above zero and below 1 trillion."
    ElseIf Not IsAmount(Me.txtVAT.Value, True) Then
        ShowProblem Me.txtVAT, "Enter VAT of zero or more, below 1 trillion."
    Else
        InputIsValid = True
    End If
End Function

Private Function IsAmount(ByVal entry As String, ByVal allowZero As Boolean) As Boolean
    Dim n As Double
    If Not IsNumeric(entry) Then Exit Function
    n = CDbl(entry)
    If n >= 1000000000000# Then Exit Function
    IsAmount = (n > 0) Or (allowZero And n = 0)
End Function

Private Sub ShowProblem(ByVal ctl As MSForms.Control, ByVal message As String)
    MsgBox message, vbExclamation, "Check this entry"
    ctl.SetFocus
End Sub

For instant feedback on one field, BeforeUpdate can keep focus in the box until it is fixed. Use it sparingly. While the entry is invalid, focus cannot move to other controls, so the user can't click away until it is fixed.

The invoice edit UserForm running in Excel with a record loaded and the message Net must be a number
The form from this guide running in Excel: invoice HS-4431 picked from the list, and txtNet_BeforeUpdate holding the cursor in Net after "abc" was typed.
Private Sub txtNet_BeforeUpdate(ByVal Cancel As MSForms.ReturnBoolean)
    If Len(Me.txtNet.Value) > 0 And Not IsNumeric(Me.txtNet.Value) Then
        Cancel = True
        Me.lblStatus.Caption = "Net must be a number."
    Else
        Me.lblStatus.Caption = vbNullString
    End If
End Sub

How to write UserForm data back to a table

ListRows(n).Range returns data row n of the table, counting from 1; the header and total rows aren't part of ListRows (ListObject.ListRows). Writing by column name survives inserted columns; the calculated Gross column is left alone. A pivot table picks up the change when you refresh the pivot table.

Private Sub btnSave_Click()
    Dim lo As ListObject
    Dim r As Range
    Dim oldFormulas As Variant
    Dim failMsg As String

    On Error GoTo SaveFailed
    If Not InputIsValid() Then Exit Sub

    Set lo = ThisWorkbook.Worksheets("Register").ListObjects("tblPurchaseRegister")
    Set r = lo.ListRows(m_Row).Range
    oldFormulas = r.Formula                  ' snapshot for undo
    r.Cells(1, lo.ListColumns("Date").Index).Value = CDate(Me.txtDate.Value)
    r.Cells(1, lo.ListColumns("Supplier").Index).Value = Me.cboSupplier.Value
    r.Cells(1, lo.ListColumns("Net").Index).Value = _
        Application.WorksheetFunction.Round(CDbl(Me.txtNet.Value), 2)
    r.Cells(1, lo.ListColumns("VAT").Index).Value = _
        Application.WorksheetFunction.Round(CDbl(Me.txtVAT.Value), 2)

    LoadInvoiceList                          ' show the corrected values
    m_SavedCount = m_SavedCount + 1
    Exit Sub

SaveFailed:
    failMsg = Err.Description
    Resume RestoreRow
RestoreRow:
    On Error Resume Next
    If Not IsEmpty(oldFormulas) Then r.Formula = oldFormulas
    On Error GoTo 0
    MsgBox "The change was not saved: " & failMsg, vbExclamation, "Invoice edit"
End Sub

If anything fails part-way, the handler restores the row's original values and formulas. After a save, Save stays disabled until a row is picked again. To add rows instead, use ListRows.Add.

Excel register table with a corrected invoice row next to the invoice edit UserForm after saving
After Save: the register row reads Net 1,000.00 and VAT 200.00, the Gross formula gives 1,200.00, the list shows the new figure and Save is disabled until another row is picked.

How to close a UserForm in VBA

Use Unload Me when the form does all its own work, or Me.Hide when a calling macro still needs its values. Either way, handle the title-bar X: it raises QueryClose, whose CloseMode says why the form is closing (QueryClose event).

CloseMode constantValueCause
vbFormControlMenu0The user clicked X or the Control menu's Close
vbFormCode1Code ran an Unload statement
vbAppWindows2The Windows session is ending (sign-out or shutdown)
vbAppTaskManager3Task Manager is closing Excel
Private Sub btnClose_Click()
    Me.Hide
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CloseMode = vbFormControlMenu Then
        Cancel = True        ' keep the form in memory...
        Me.Hide              ' ...and treat X exactly like Close
    End If
End Sub

As Integer is the event's required signature. Routing X through Close gives the calling macro one outcome.

Reusing a form: let the calling macro own it

Because this form hides itself, the default-instance macro would show the old selection next time. Instead, create a new instance with New (UserForm object), show it, read its properties, then unload it.

Option Explicit

Public Sub OpenInvoiceEdit()
    Dim frm As frmInvoiceEdit
    On Error GoTo Failed

    Set frm = New frmInvoiceEdit    ' Initialize runs here
    frm.Show vbModal                ' the macro waits until the form hides
    If frm.SavedCount > 0 Then
        MsgBox frm.SavedCount & " invoice change(s) saved.", vbInformation, "Invoice edit"
    End If
    Unload frm
    Exit Sub

Failed:
    MsgBox "Invoice edit stopped: " & Err.Description, vbExclamation, "Invoice edit"
    Resume CleanUp                  ' leave error-handling mode first
CleanUp:
    On Error Resume Next
    If Not frm Is Nothing Then Unload frm
End Sub

Replace OpenInvoiceEditSimple with this (keep one Option Explicit per module) and attach OpenInvoiceEdit to the sheet button. If someone renames the Lists sheet, the macro shows a message instead of stopping in the editor (with VBA's default error trapping).

64-bit Excel and UserForms

64-bit has been the default install since Office 2019 and Microsoft 365 (64-bit VBA overview). The code above makes no Windows API calls, so it needs no changes. Older form code trips on two things:

#If VBA7 Then
    Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
        (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
#Else
    Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
        (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
#End If

Do UserForms work on a Mac, in Excel for the web or with ActiveX disabled?

Not in Excel for the web. On a Mac they generally run, but community experts say you can't design them there. Microsoft's ActiveX change doesn't mention UserForms.

Office Scripts isn't a drop-in replacement: scripts reach only the workbook and run when a user or Power Automate starts them (Office Scripts vs VBA).

How HISAB 360 builds and checks UserForms

HISAB 360's AI builds UserForms in Windows desktop Excel as a guided multi-step workflow in chat, not a one-click generator.

We asked HISAB to lint the form built in this guide. Its AI confirmed the WithEvents wiring and the error handling, and pointed out that the Export handler is only a stub. The linter itself raised false positives on this working code: controls and properties set in the form designer (it reads the code, not the designer), event handlers reported as unused, and one Error-level finding on the standard Cancel = True in txtNet_BeforeUpdate — exactly the kind of finding that makes the write gate reject working code. HISAB's AI marked each of them as a false positive instead of changing the code.

HISAB 360 panel in Excel reviewing the UserForm code and listing lint findings that are false positives
HISAB 360 reviewing this guide's form: the real finding (a stub handler) and the linter warnings it identified as false positives.

The limits, plainly:

Not for: anyone who wants to lay out controls by hand in the VBA editor, or teams whose IT blocks VBA project access. For a one-off macro, try the free VBA macro generator, or see building VBA macros and a ribbon from chat.

Start free → See pricing