RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Reference

UI

BeamNG ImGui API ReferenceBeamNG ImGui Cheat SheetBeamNG ImGui Data TypesBeamNG World Editor ImGui WindowsBeamNG ImGui Flags & ConstantsBeamNG ImGui Patterns & ExamplesBeamNG ImGui Style Guide

Resources

BeamNG Game Engine Lua Cheat SheetGE Developer RecipesMCP Server Setup

// RLS.STUDIOS=true

Premium Mods for BeamNG.drive. Career systems, custom vehicles, and immersive gameplay experiences.

Index

HomeProjectsPatreon

Socials

DiscordPatreon (RLS)Patreon (Vehicles)

© 2026 RLS Studios. All rights reserved.

Modding since 2024

ImGui

BeamNG ImGui Style Guide

Visual patterns and conventions extracted from the Freeroam Event Editor. Use these patterns for consistent, polished editor UIs.

Visual patterns and conventions extracted from the Freeroam Event Editor. Use these patterns for consistent, polished editor UIs.

Semantic Color Palette

Define colors at the top of your file for consistent use throughout:

local colors = {
  success = im.ImVec4(0.2, 0.8, 0.2, 1.0),   -- Green - valid, complete, enabled
  warning = im.ImVec4(1.0, 0.8, 0.2, 1.0),   -- Yellow/Orange - caution, pending
  error = im.ImVec4(0.9, 0.2, 0.2, 1.0),     -- Red - invalid, missing, danger
  info = im.ImVec4(0.4, 0.7, 1.0, 1.0),      -- Blue - informational, selected
  dimmed = im.ImVec4(0.6, 0.6, 0.6, 1.0),    -- Gray - secondary, labels
  highlight = im.ImVec4(0.95, 0.43, 0.49, 1.0), -- Pink - accent, attention
}

Use with im.TextColored(colors.success, "Valid!").

Section Separators

Use im.SeparatorText() for major sections:

im.SeparatorText("Basic Information")
-- inputs here...

im.SeparatorText("Event Options")
-- more inputs...

This creates a horizontal line with centered text - much cleaner than plain im.Separator().

Help Markers

Add contextual help with a (?) tooltip pattern:

local function helpMarker(text, sameLine)
  if sameLine then im.SameLine() end
  im.TextDisabled("(?)")
  if im.IsItemHovered() then
    im.BeginTooltip()
    im.PushTextWrapPos(im.GetFontSize() * 25)
    im.TextUnformatted(text)
    im.PopTextWrapPos()
    im.EndTooltip()
  end
end

-- Usage:
im.Checkbox("Running Start", runningStart)
helpMarker("Timer starts when player crosses start line at speed.", true)

Two-Panel Layout

Standard editor layout with list on left, details on right:

local windowWidth = im.GetContentRegionAvail().x
local leftPanelWidth = windowWidth * 0.3  -- 30% for list

-- Left panel
im.BeginChild1("ItemList", im.ImVec2(leftPanelWidth, im.GetContentRegionAvail().y), true)
  -- list items here
im.EndChild()

im.SameLine()

-- Right panel
im.BeginChild1("ItemDetails", im.ImVec2(0, im.GetContentRegionAvail().y), true)
  -- detail form here
im.EndChild()

State-Colored Buttons

Color buttons based on item state:

local exists = checkIfExists()
local isSelected = (item == currentItem)

if not exists then
  -- Red for missing/incomplete
  im.PushStyleColor2(im.Col_Button, im.ImVec4(0.5, 0.2, 0.2, 1.0))
  im.PushStyleColor2(im.Col_ButtonHovered, im.ImVec4(0.6, 0.3, 0.3, 1.0))
  im.PushStyleColor2(im.Col_ButtonActive, im.ImVec4(0.7, 0.4, 0.4, 1.0))
elseif isSelected then
  -- Blue for selected
  im.PushStyleColor2(im.Col_Button, im.ImVec4(0.2, 0.4, 0.6, 1.0))
  im.PushStyleColor2(im.Col_ButtonHovered, im.ImVec4(0.3, 0.5, 0.7, 1.0))
  im.PushStyleColor2(im.Col_ButtonActive, im.ImVec4(0.4, 0.6, 0.8, 1.0))
end

if im.Button(itemLabel .. "##" .. itemId, im.ImVec2(im.GetContentRegionAvail().x, 0)) then
  currentItem = item
end

if not exists or isSelected then
  im.PopStyleColor(3)
end

Trigger Button Pattern

For create/select buttons that toggle state:

local function triggerButton(type, displayName, id)
  local exists = triggerExists(type, id)
  local isPending = (pendingType == type)
  
  local buttonText = exists and ("Select " .. displayName) or ("Create " .. displayName)
  if isPending then buttonText = "Cancel " .. displayName .. " Placement" end
  
  if isPending then
    im.PushStyleColor2(im.Col_Button, im.ImVec4(0.6, 0.4, 0.1, 1))  -- Orange for pending
  elseif exists then
    im.PushStyleColor2(im.Col_Button, im.ImVec4(0.1, 0.4, 0.2, 1))  -- Green for exists
  else
    im.PushStyleColor2(im.Col_Button, im.ImVec4(0.4, 0.1, 0.1, 1))  -- Red for missing
  end
  
  if im.Button(buttonText .. "##" .. type, im.ImVec2(im.GetContentRegionAvail().x, 0)) then
    -- toggle action
  end
  im.PopStyleColor()
end

Progress Indicators

Show completion status with progress bar:

local done, total = getCompleteness(item)
local progressColor = done == total and colors.success or colors.warning

im.TextColored(progressColor, string.format("(%d/%d)", done, total))
im.ProgressBar(done / total, im.ImVec2(-1, 0), string.format("%d/%d components", done, total))

Filter Inputs

For searchable lists, use InputTextWithHint:

local filterText = ""
local filterBuf = im.ArrayChar(128, filterText)

im.SetNextItemWidth(im.GetContentRegionAvail().x)
if im.InputTextWithHint("##Filter", "Filter items...", filterBuf, 128) then
  filterText = ffi.string(filterBuf)
end

-- In your list loop:
local filterLower = filterText:lower()
for _, item in pairs(items) do
  if filterLower ~= "" and not string.find(item.name:lower(), filterLower) then
    goto continue
  end
  -- render item
  ::continue::
end

Input Validation Feedback

Show invalid state with red background:

local isValid = validateInput(value)

if not isValid then
  im.PushStyleColor2(im.Col_FrameBg, im.ImVec4(0.5, 0.1, 0.1, 1))
end

if im.InputText("Field", valueBuf) then
  -- handle input
end

if not isValid then
  im.PopStyleColor()
  im.SameLine()
  im.TextColored(colors.error, "!")
  if im.IsItemHovered() then
    im.SetTooltip("Validation error message here")
  end
end

Tables for Data Display

Use tables for organized key-value or columnar data:

if im.BeginTable("DataTable", 2, im.TableFlags_BordersInnerV) then
  im.TableSetupColumn("Label", im.TableColumnFlags_WidthStretch)
  im.TableSetupColumn("Value", im.TableColumnFlags_WidthStretch)
  
  im.TableNextRow()
  im.TableSetColumnIndex(0)
  im.Text("Average Gap")
  im.TableSetColumnIndex(1)
  im.Text(formatValue(avgGap))
  
  -- more rows...
  
  im.EndTable()
end

Modal Confirmations

For destructive actions, use a confirmation modal:

-- Trigger
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.6, 0.1, 0.1, 1))
if im.Button("Delete Item", im.ImVec2(im.GetContentRegionAvail().x, 30)) then
  im.OpenPopup("Delete Confirmation")
end
im.PopStyleColor()

-- Modal
if im.BeginPopupModal("Delete Confirmation", nil, im.WindowFlags_AlwaysAutoResize) then
  im.Text("Are you sure you want to delete this?")
  im.TextColored(colors.warning, itemName)
  im.Text("This action cannot be undone.")
  im.Separator()
  
  im.PushStyleColor2(im.Col_Button, im.ImVec4(0.6, 0.1, 0.1, 1))
  if im.Button("Yes, Delete", im.ImVec2(im.GetContentRegionAvail().x * 0.48, 0)) then
    deleteItem()
    im.CloseCurrentPopup()
  end
  im.PopStyleColor()
  
  im.SameLine()
  
  if im.Button("Cancel", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
    im.CloseCurrentPopup()
  end
  
  im.EndPopup()
end

Indentation for Nested Options

Use indent/unindent for conditional sub-options:

local hasOption = im.BoolPtr(data.option ~= nil)
if im.Checkbox("Enable Option", hasOption) then
  if hasOption[0] then
    data.option = defaultValue
  else
    data.option = nil
  end
end

if hasOption[0] then
  im.Indent()
  
  local optionValue = im.FloatPtr(data.option)
  if im.InputFloat("Option Value", optionValue) then
    data.option = optionValue[0]
  end
  
  im.Unindent()
end

Collapsible Sections

Use CollapsingHeader for optional/advanced details:

if im.CollapsingHeader1("Advanced Options") then
  -- detailed content here
end

-- With default open:
if im.CollapsingHeader1("Details##section", im.TreeNodeFlags_DefaultOpen) then
  -- content
end

Rich Tooltips on Hover

Add detailed tooltips to list items:

if im.Button(itemLabel) then
  selectItem(item)
end

if im.IsItemHovered() then
  im.BeginTooltip()
  im.Text("ID: " .. item.id)
  
  local done, total = getProgress(item)
  if done == total then
    im.TextColored(colors.success, "Complete!")
  else
    im.TextColored(colors.warning, string.format("Progress: %d/%d", done, total))
    im.Separator()
    for _, component in ipairs(getMissing(item)) do
      im.TextColored(colors.error, "✗ " .. component)
    end
  end
  
  im.EndTooltip()
end

Money/Number Formatting

Format large numbers for readability:

local function formatMoney(amount)
  amount = math.floor(amount + 0.5)
  if amount >= 1000000 then
    return string.format("$%.1fM", amount / 1000000)
  elseif amount >= 1000 then
    return string.format("$%dk", math.floor(amount / 1000))
  else
    return string.format("$%d", amount)
  end
end

Status Line Pattern

Show save state and context at top of window:

if modified then
  im.TextColored(colors.warning, "* Modified (unsaved)")
else
  im.TextColored(colors.success, "Saved")
end
im.SameLine()
im.TextColored(colors.dimmed, "| Level: " .. currentLevel)

im.Separator()

Full-Width Buttons

For action buttons, use full width:

-- Single button
if im.Button("Action", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
  doAction()
end

-- Two buttons side by side
if im.Button("Left", im.ImVec2(im.GetContentRegionAvail().x * 0.48, 0)) then
  leftAction()
end
im.SameLine()
if im.Button("Right", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
  rightAction()
end

-- Tall action button
if im.Button("Important Action", im.ImVec2(im.GetContentRegionAvail().x, 30)) then
  doImportantThing()
end

Add/Remove List Pattern

For dynamic lists with add/remove:

for i, item in ipairs(items) do
  im.PushID1("item_" .. tostring(i))
  
  -- Remove button (except first item)
  if i > 1 then
    if im.Button("X##remove", im.ImVec2(24, 0)) then
      table.remove(items, i)
      im.PopID()
      break  -- exit loop, list modified
    end
    im.SameLine()
  end
  
  -- Item content
  drawItemContent(item, i)
  
  im.PopID()
end

-- Add button at bottom
if im.Button("+ Add Item", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
  table.insert(items, createNewItem())
end

Graph Visualization

For PlotLines with readable tooltips:

local function getScaleInfo(maxVal)
  if maxVal >= 1000000 then return 1000000, "M"
  elseif maxVal >= 1000 then return 1000, "k"
  else return 1, ""
  end
end

local function drawPlot(label, dataTable, maxVal, height)
  local divisor, suffix = getScaleInfo(maxVal)
  local scaledTable = {}
  for i, v in ipairs(dataTable) do
    scaledTable[i] = v / divisor
  end
  
  local data = im.TableToArrayFloat(scaledTable)
  local dataLen = im.GetLengthArrayFloat(data)
  local scaledMax = math.max((maxVal / divisor) * 1.1, 0.01)
  
  local overlayText = suffix ~= "" and ("$" .. suffix) or "$"
  im.PlotLines1(label, data, dataLen, 0, overlayText, 0, scaledMax, 
    im.ImVec2(im.GetContentRegionAvail().x, height))
end

Complete Example Structure

local M = {}
local im = ui_imgui
local ffi = require("ffi")

-- ============================================================================
-- COLORS
-- ============================================================================
local colors = {
  success = im.ImVec4(0.2, 0.8, 0.2, 1.0),
  warning = im.ImVec4(1.0, 0.8, 0.2, 1.0),
  error = im.ImVec4(0.9, 0.2, 0.2, 1.0),
  info = im.ImVec4(0.4, 0.7, 1.0, 1.0),
  dimmed = im.ImVec4(0.6, 0.6, 0.6, 1.0),
}

-- ============================================================================
-- HELPERS
-- ============================================================================
local function helpMarker(text, sameLine)
  if sameLine then im.SameLine() end
  im.TextDisabled("(?)")
  if im.IsItemHovered() then
    im.BeginTooltip()
    im.PushTextWrapPos(im.GetFontSize() * 25)
    im.TextUnformatted(text)
    im.PopTextWrapPos()
    im.EndTooltip()
  end
end

-- ============================================================================
-- MAIN GUI
-- ============================================================================
local function onEditorGui()
  if editor.beginWindow(toolWindowName, "My Editor", im.WindowFlags_MenuBar) then
    -- Menu bar
    if im.BeginMenuBar() then
      if im.BeginMenu("File") then
        if im.MenuItem1("Save") then saveData() end
        im.EndMenu()
      end
      im.EndMenuBar()
    end
    
    -- Status line
    if modified then
      im.TextColored(colors.warning, "* Modified")
    else
      im.TextColored(colors.success, "Saved")
    end
    im.Separator()
    
    -- Two-panel layout
    local leftWidth = im.GetContentRegionAvail().x * 0.3
    
    im.BeginChild1("List", im.ImVec2(leftWidth, -1), true)
      -- list content
    im.EndChild()
    
    im.SameLine()
    
    im.BeginChild1("Details", im.ImVec2(0, -1), true)
      im.SeparatorText("Section 1")
      -- form content
      
      im.SeparatorText("Section 2")
      -- more content
    im.EndChild()
    
    editor.endWindow()
  end
end

M.onEditorGui = onEditorGui
return M

BeamNG ImGui Patterns & Examples

local im = ui_imgui

BeamNG Game Engine Lua Cheat Sheet

A collection of 50+ high-frequency one-liners and snippets for game engine-side scripting.

On this page

Semantic Color PaletteSection SeparatorsHelp MarkersTwo-Panel LayoutState-Colored ButtonsTrigger Button PatternProgress IndicatorsFilter InputsInput Validation FeedbackTables for Data DisplayModal ConfirmationsIndentation for Nested OptionsCollapsible SectionsRich Tooltips on HoverMoney/Number FormattingStatus Line PatternFull-Width ButtonsAdd/Remove List PatternGraph VisualizationComplete Example Structure