User interface reference


Every widget is defined by a layout, which is a Lua table with the following fields (all of them are optional):

  1. type: One of the available widget types from openmw.ui.TYPE.

  2. props: A Lua table, containing all the properties values.
    Properties define most of the information about the widget: its position, data it displays, etc.
    See the widget pages (table below) for details on specific properties.
    Properties of the basic Widget are inherited by all the other widgets.
  3. events: A Lua table, containing openmw.async.callback values, which trigger on various interactions with the widget.
    See the Widget pages for details on specific events.
    Events of the basic Widget are inherited by all the other widgets.
  4. content: a Content (openmw.ui.content), which contains layouts for the children of this widget.

  5. name: an arbitrary string, the only limitatiion is it being unique within a Content.
    Helpful for navigatilng through the layouts.
  6. layer: only applies for the root widget.


Layers control how widgets overlap - layers with higher indexes cover render over layers with lower indexes. Widgets within the same layer which were added later overlap the ones created earlier. A layer can also be set as non-interactive, which prevents all mouse interactions with the widgets in that layer.

Pre-defined OpenMW layers:

  1. HUD interactive
  2. Windows interactive
  3. Notification non-interactive
  4. MessageBox interactive


Element is the root widget of a layout. It is an independent part of the UI, connected only to a specific layer, but not any other layouts. Creating or destroying an element also creates/destroys all of its children.


A container holding all the widget’s children. It has a few important differences from a Lua table:

  1. All the keys are integers, i. e. it is an “array”

  2. Holes are not allowed. At any point all keys from 1 to the highest n must contain a value.

  3. You can access the values by their name field as a Content key.
    While there is nothing preventing you from changing the name of a table inside a content, it is not supported, and will lead to undefined behaviour.
    If you have to change the name, assign a new table to the index instead.


A table mapping event names to openmw.async.callback s.
When an event triggers, the callback is called with two arguments: an event-specific value, and that widget’s layout table.
See the Widget type pages for information on what events exist, and which first argument they pass.

Widget types

Widget Base widget type, all the other widget inherit its properties and events.
Text Displays text.
EditText Accepts text input from the user.
Window Can be moved and resized by the user.



local core = require('openmw.core')
local async = require('openmw.async')
local ui = require('openmw.ui')
local v2 = require('openmw.util').vector2

local layout = {
   layers = 'Windows',
   type = ui.TYPE.Window,
   skin = 'MW_Window', -- TODO: replace all skins here when they are properly implemented
   props = {
      size = v2(200, 250),
      -- put the window in the middle of the screen
      relativePosition = v2(0.5, 0.5),
      anchor = v2(0.5, 0.5),
   content = ui.content {
         type = ui.TYPE.Text,
         skin = 'SandText',
         props = {
            caption = 'Input password',
            relativePosition = v2(0.5, 0),
            anchor = v2(0.5, 0),
         name = 'input',
         type = ui.TYPE.TextEdit,
         skin = "MW_TextEdit",
         props = {
            caption = '',
            relativePosition = v2(0.5, 0.5),
            anchor = v2(0.5, 0.5),
            size = v2(125, 50),
         events = {}
         name = 'submit',
         type = ui.TYPE.Text, -- TODO: replace with button when implemented
         skin = "MW_Button",
         props = {
            caption = 'Submit',
            -- position at the bottom
            relativePosition = v2(0.5, 1.0),
            anchor = v2(0.5, 1.0),
            autoSize = false,
            size = v2(75, 50),
         events = {},

local element = nil

local input = layout.content.input
-- TODO: replace with a better event when TextEdit is finished = async:callback(function(text)
   input.props.caption = input.props.caption .. text

local submit = layout.content.submit = async:callback(function()
   if input.props.caption == 'very secret password' then
      if element then
      print('wrong password', input.props.caption)

element = ui.create(layout)


PLAYER: scripts/requirePassword.lua