Writing World of Warcraft Addons in Lua
Overview
World of Warcraft supports Lua addons dating back to the original 2004 release. Every part of the WoW UI — action bars, the character panel, raid frames — is built as an addon using the same API available to third-party developers. The client embeds a modified Lua 5.1 interpreter with access to a large API surface for UI manipulation, event handling, networking, and data storage.
The key difference from standard Lua is the execution model. WoW addons are event-driven: your code registers handlers for events, and the game calls those handlers when things happen. There is no require() — all inter-addon communication uses the shared WoW API directly.
Addon structure
An addon lives in a folder under World of Warcraft/Interface/AddOns/. The folder must contain a .toc file that tells WoW how to load it.
MyAddon/
MyAddon.toc -- metadata, load order
MyAddon.lua -- main script
MyAddon.xml -- XML UI definitions (optional)
The .TOC file
## Interface: 100205
## Title: MyAddon
## Notes: A useful addon.
## Author: YourName
## Version: 1.0.0
MyAddon.lua
MyAddon.xml
## Interface specifies which WoW patch version the addon targets. 100205 means patch 10.2.5. WoW refuses to load addons with a mismatched interface number.
Loading order
Dependencies load before dependents. List them in the .toc:
## Dependencies: LibStub, AceAddon-3.0
If your addon uses a library like Ace3, it loads after that library is available.
Your first addon
Myaddon.lua
-- MyAddon.lua
local _, ns = ...
ns.version = "1.0.0"
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:SetScript("OnEvent", function(self, event, ...)
if event == "PLAYER_LOGIN" then
print("MyAddon loaded! Version: " .. ns.version)
end
end)
PLAYER_LOGIN fires once when the player character is fully loaded. CreateFrame("Frame") creates a generic frame to hold your event handlers.
The event system
Registering and handling events
local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_WHISPER")
f:RegisterEvent("PLAYER_LEVEL_UP")
f:RegisterEvent("UNIT_HEALTH")
f:SetScript("OnEvent", function(self, event, ...)
if event == "CHAT_MSG_WHISPER" then
local message, sender = ...
print("Whisper from " .. sender .. ": " .. message)
elseif event == "PLAYER_LEVEL_UP" then
local newLevel = select(2, ...)
print("You reached level " .. newLevel .. "!")
elseif event == "UNIT_HEALTH" then
local unitId = ...
-- health changed for this unit
end
end)
OnEvent receives the triggering event name first, then any arguments specific to that event.
Unregistering events
frame:UnregisterEvent("UNIT_HEALTH")
frame:UnregisterAllEvents() -- clean up on disable
Slash commands
Registering a slash command
SLASH_MYADDON1 = "/myaddon"
SLASH_MYADDON2 = "/ma"
SlashCmdList["MYADDON"] = function(msg)
if msg == "config" then
OpenConfig()
elseif msg == "version" then
print("MyAddon " .. ns.version)
else
print("Usage: /myaddon config | version")
end
end
Slash commands are global — use a prefix tied to your addon name. Define both short and long forms via numbered suffixes.
Saved variables
SavedVariables persist data between game sessions. WoW serializes them to a file when the client exits and reloads them when your addon loads.
Declaring saved variables in .TOC
## SavedVariablesPerCharacter: MyAddonDB
MyAddonDB is created as a global when the character loads. Use a character-scoped table for per-character settings, or a global table for account-wide settings.
Initializing the database
MyAddonDB = MyAddonDB or {}
local defaults = {
enabled = true,
threshold = 50,
notify = false,
}
for k, v in pairs(defaults) do
if MyAddonDB[k] == nil then
MyAddonDB[k] = v
end
end
Use the or pattern to avoid overwriting existing settings on upgrade.
Per-character VS account-wide
| Declaration | Scope |
|---|---|
## SavedVariables: MyAddonConfig | Account-wide |
## SavedVariablesPerCharacter: MyAddonDB | Per character |
Chat frames and messaging
Sending chat messages
C_ChatInfo.SendChatMessage("Hello world!", "SAY")
-- Send to a specific channel
C_ChatInfo.SendChatMessage("Hello channel!", "CHANNEL", nil, channelNumber)
Use the C_ChatInfo API rather than the older SendChatMessage() — it handles channel registration correctly.
Receiving messages
frame:RegisterEvent("CHAT_MSG_WHISPER")
frame:RegisterEvent("CHAT_MSG_GUILD")
frame:RegisterEvent("CHAT_MSG_PARTY")
frame:SetScript("OnEvent", function(self, event, message, sender, ...)
if event == "CHAT_MSG_WHISPER" then
-- sender is the character who whispered
end
end)
Creating a custom button
local button = CreateFrame("Button", "MyAddonButton", UIParent, "UIPanelButtonTemplate")
button:SetSize(80, 24)
button:SetPoint("CENTER", UIParent, "CENTER", 0, -200)
button:SetText("Click Me")
button:SetScript("OnClick", function()
print("Button clicked!")
end)
The WoW Lua API
Timers
C_Timer.After(5, function()
print("This runs after 5 seconds.")
end)
C_Timer.After(0.1, function()
print("This runs after 100 milliseconds.")
end)
C_Timer.After is non-blocking and runs after the specified delay. Multiple timers can run concurrently.
Units
WoW identifies game entities with unit IDs:
UnitName("player") -- your character
UnitName("target") -- your current target
UnitName("party1") -- first party member
UnitName("raid1") -- first raid member
UnitName("mouseover") -- unit under the mouse
Fetch unit attributes:
UnitHealth("player") -- current health
UnitHealthMax("player") -- maximum health
UnitPower("player") -- current primary power (rage, mana, etc.)
UnitLevel("target") -- level of your target
UnitIsPlayer("target") -- true if target is a player
UnitFactionGroup("target") -- "Alliance" or "Horde"
Inspecting the environment
-- Get the current zone name
local zoneName = GetRealZoneText()
-- What class is the player?
local _, class = UnitClass("player")
-- What spec does the player have (for dual specialization)?
local specId = GetSpecialization()
local specName = select(2, GetSpecializationInfo(specId))
XML layouts
WoW’s UI is defined in XML. You can create frames entirely in Lua, but XML provides a cleaner structure for complex layouts.
A simple XML frame
<Ui xmlns="http://www.blizzard.com/wiki/ui/">
<Frame name="MyAddonFrame" parent="UIParent">
<Size x="200" y="100"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture>
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentText" text="Hello" inherits="GameFontNormal">
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</Ui>
Load the XML in your .toc with the filename:
MyAddon.lua
MyAddon.xml
In Lua, refer to the frame by the name attribute: MyAddonFrame is a global.
Common patterns
Initialization guard
local ADDON_NAME, ns = ...
local frame = CreateFrame("Frame")
local function OnEvent(self, event, ...)
if event == "PLAYER_LOGIN" then
-- Initialize addon
ns.initialized = true
frame:UnregisterAllEvents()
end
end
frame:SetScript("OnEvent", OnEvent)
frame:RegisterEvent("PLAYER_LOGIN")
-- Also fire for already-loaded addon (UI reload)
if IsLoggedIn() then
frame:GetScript("OnEvent")(frame, "PLAYER_LOGIN")
end
Modular organization
-- MyAddon/core.lua
local _, ns = ...
function ns.Init()
-- init logic
end
-- MyAddon/events.lua
local _, ns = ...
local f = CreateFrame("Frame")
f:SetScript("OnEvent", function(self, event, ...)
-- handle events
end)
Split logic across files in the same addon folder. Each file runs in the same global environment.
Checking if another addon is loaded
if IsAddOnLoaded("SomeOtherAddon") then
-- interact with SomeOtherAddon's global API
end
Gotchas
No require() or dofile(). There is no filesystem access and no module loader. All Lua files in your addon’s folder are loaded automatically in alphabetical order. Share state through global namespaces or through a library like Ace3’s :NewModule().
Slash command names must be unique globally. Choose a prefix unlikely to collide. Use MYADDON (all caps) as the command list key.
UI is hidden during loading screens. PLAYER_LOGIN fires after the character is loaded but before entering the world. For code that needs the world to be available, use PLAYER_ENTERING_WORLD.
The secure execution environment. Certain functions like hooksecurefunc() and SecureHandler are used to modify protected UI elements without violating the anti-cheat system. Do not attempt to hook or modify protected functions outside of documented secure patterns.
WoW’s Lua is Lua 5.1. Bitwise operations are through bit.band(), bit.bor(), etc. — there is no native bitwise syntax. Pattern matching works as standard Lua patterns.
Never assume a global exists. Check with if MyAddonDB then before using a saved variable on first load.
See also
- /guides/lua-environments/ — environment and settings management patterns
- /guides/lua-string-patterns/ — Lua patterns for parsing chat messages and unit names
- /guides/lua-metatables/ — metatables for building addon object systems and custom data structures