Skip to content
CyanideX edited this page Aug 5, 2026 · 1 revision

Tabs

Styled tab bars with badge indicators, disabled tabs, programmatic selection, and badge visual customization.

Quick Start

local wu = GetMod("WindowUtils")
local tabs = wu.Tabs

tabs.bar("myTabs", {
    { label = "General", content = function()
        ImGui.Text("General settings here")
    end },
    { label = "Advanced", content = function()
        ImGui.Text("Advanced settings here")
    end },
})

API Reference

bar(id, tabDefs, opts?)

Render a tab bar with content callbacks.

Parameter Type Default Description
id string - Unique tab bar ID
tabDefs table - Array of tab definitions (see below)
opts.flags number 0 ImGuiTabBarFlags

Returns: two values, selected (number, 1-based active tab index) and changed (boolean, true on any frame where the active index differs from the previous frame).

When tabDefs is nil or empty, bar renders nothing and returns 0, false. Note that getSelected(id) returns 1 in that same situation, because it defaults to the first tab rather than reporting "no tabs". Only bar uses 0.

changed is not strictly "the user clicked": it is also true on the frame a select() request lands, and on any frame where the active index moves for some other reason (a tab being removed, for instance).

Tab Definition

Each entry in tabDefs is a table:

Field Type Default Description
label string - Tab label text
content function|nil nil Callback to render tab content
badge boolean|number|nil nil Badge indicator (see below)
badgeStyle table|nil nil Per-tab badge visual overrides
disabled boolean false Grey out, prevent selection, and skip the content callback
tooltip string|nil nil Tooltip on tab hover
noScroll boolean false Wrap content in a no-scroll child that fills available space

A disabled tab renders no content at all, even when it is the selected tab (for example if it became disabled while active). Its content callback is skipped entirely, so nothing from it stays interactive.

Badges

  • badge = true - small green dot indicator
  • badge = 3 - red circle with number "3" (notification count)
  • badge = nil - no badge
  • badge = 0, badge = false, or any negative number - no badge, so you can pass a raw counter without guarding it
  • disabled = true suppresses the badge as well as the interaction

Per-Tab Badge Style

Override global badge appearance for a specific tab. badgeStyle only affects numeric badges; the badge = true dot is always green and ignores it.

{
    label = "Alerts",
    badge = 5,
    badgeStyle = {
        color = { 1.0, 0.5, 0.0, 0.9 },  -- orange, 90% opacity
        fontScale = 0.8,
        offset = 1,
    },
    content = drawAlerts,
}

select(id, index)

Programmatically select a tab by 1-based index. Takes effect on the next frame the bar renders. Works before the bar has ever rendered (state is created on demand). The index is not validated: an out-of-range value is consumed and nothing changes.

getSelected(id)

Get the currently selected tab index.

Returns: number - 1-based index, or 1 if the id has never rendered, has never been selected, or was destroyed. It never returns 0 or nil, so it cannot tell you "no tabs"; bar returns 0 for that case.

setOnSelect(fn)

Register a callback invoked when any tab bar's active index changes, including changes caused by select(). The callback receives (barId, newIndex).

There is one global callback, not one per bar, so a second call replaces the first. Pass nil to clear.

Parameter Type Description
fn function|nil Callback (barId, newIndex), or nil to clear

configure(opts)

Configure badge visual defaults. Affects all badges that don't have per-tab badgeStyle overrides.

Parameter Type Description
opts.badgeConfig table|nil Badge visual configuration

badgeConfig Options

Option Type Default Description
color table red RGBA table for circle fill (alpha controls opacity, e.g. {1, 0, 0, 0.8}). Text color auto-switches between white and black based on luminance.
fontScale number 0.8 Font size multiplier (base 36px, keep at or below 1.0 to avoid blur)
offset number -5 Badge offset from tab corner in pixels (negative = closer to corner)
tabs.configure({
    badgeConfig = {
        fontScale = 0.7,
        color = { 0.2, 0.6, 1.0, 0.85 },   -- blue with 85% opacity
        offset = 2,
    }
})

getBadgeConfig()

Returns: table - current badge config state (for inspection)

This is the module's live config table, not a copy. Writing to it changes the global badge defaults without going through configure(), which means the cached badge colors are not invalidated and your new color will not appear until something else triggers a rebuild. Read it, do not mutate it.

destroy(id)

Remove internal state for a tab bar ID. Call when dynamically created tab bars are no longer needed. Any pending select() is dropped and getSelected(id) goes back to reporting 1.

Examples

Tabs with Badges and Disabled State

tabs.bar("settings", {
    { label = "General", content = drawGeneral },
    { label = "Notifications", badge = unreadCount, content = drawNotifications },
    { label = "Debug", disabled = not debugMode, tooltip = "Enable debug mode first",
      content = drawDebug },
    { label = "Status", badge = isConnected, content = drawStatus },
})

Clearing Badges on Tab Select

local selected, changed = tabs.bar("settings", {
    { label = "General", content = drawGeneral },
    { label = "Inbox", badge = unreadCount > 0 and unreadCount or nil, content = drawInbox },
})
if changed and selected == 2 then
    unreadCount = 0
end

Badge Customization

-- Global badge style
tabs.configure({
    badgeConfig = {
        color = { 1.0, 0.4, 0.0, 0.9 },  -- orange, 90% opacity
        fontScale = 0.7,
    }
})

-- Per-tab override (this tab gets blue badges)
tabs.bar("main", {
    { label = "Chat", badge = msgCount, badgeStyle = {
        color = { 0.2, 0.5, 1.0, 1.0 },
    }, content = drawChat },
    { label = "Alerts", badge = alertCount, content = drawAlerts },  -- uses global orange
})

Tab Selection with Category Clearing

-- Bridge tab selection to notification clearing
tabs.setOnSelect(function(barId, newIndex)
    if barId == "##main_tabs" and newIndex == 2 then
        wu.Notify.clearCategory("inbox")
    end
end)

Programmatic Tab Selection

-- Switch to tab 2 when a condition is met
if shouldShowAdvanced then
    tabs.select("settings", 2)
end

-- Read which tab is active
local current = tabs.getSelected("settings")

Using Tab Bar Flags

tabs.bar("fitted", tabDefs, {
    flags = ImGuiTabBarFlags.FittingPolicyResizeDown
})

Clone this wiki locally