Custom themes

Registering your own palettes, sharing them as JSON, and the built-in theme editor.

Registering one#

A theme is a table of role names to Color3. Supply only the roles you want to change; every role you leave out takes its colour from Dark.

Luau
Ember.RegisterTheme("Sunset", {
    bg      = Color3.fromRGB(24, 16, 22),
    panel   = Color3.fromRGB(32, 22, 30),
    panel2  = Color3.fromRGB(41, 28, 38),
    accent  = Color3.fromRGB(255, 122, 47),
    accentInk = Color3.fromRGB(28, 12, 4),
})
 
Ember.SetTheme("Sunset")

Once registered, the name appears in Ember.ThemeNames() like a built-in, so a theme dropdown picks it up with no extra work. It will not appear if Themes.Only is set and does not list it.

The full API#

FunctionWhat it does
Ember.RegisterTheme(name, palette)Adds or replaces a custom theme, and writes it to disk. Returns true, name, or false, reason. A built-in name is rejected.
Ember.RemoveTheme(name)Deletes a custom theme. Built-ins are protected.
Ember.SetTheme(name)Applies it, repainting everything on screen
Ember.ThemeNames()Every selectable theme name
Ember.IsBuiltInTheme(name)Whether it shipped with the library
Ember.SaveThemes()Writes your custom themes to disk. false if the write failed.
Ember.LoadThemes()Reads them back; returns how many were loaded
Ember.ExportTheme(name)Returns the theme as a JSON string, or nil
Ember.ImportTheme(source, name?)Takes JSON or an http(s) URL. Returns the registered name, or nil, reason.
Ember.OnThemeApplied(fn)Runs fn(name) when a named theme is applied. Returns a subscription with :Disconnect().
Ember.OnThemesChanged(fn)Runs fn(names) when a theme is added or removed. Same subscription.

Persisting your themes#

RegisterTheme writes to disk on its own. Read them back on startup:

Luau
Ember.LoadThemes()   --> number of themes restored

Ember.Configure({ Storage =}) calls LoadThemes for you, so a script that configures storage already has its custom themes back. Ember.SaveThemes() is there for when you edit a palette table in place and want that written out.

Sharing one#

ExportTheme returns the palette as JSON:

Luau
local json = Ember.ExportTheme("Sunset")
setclipboard(json)

Importing validates before it applies, so a malformed string changes nothing:

Luau
local name, err = Ember.ImportTheme(json)
if not name then
    warn("that is not a theme:", err)
end

The built-in editor#

One call builds a whole section for editing themes:

Luau
win:ThemeEditor({ Name = "Theme Editor", Icon = "droplet" })

The section holds a base-theme picker, a swatch grid for every role, a colour picker for the selected role, a name box, and buttons for apply, save, delete, export and import. Export copies JSON to the clipboard. Import reads pasted JSON or fetches a URL.

Name and Icon set the section's tab, defaulting to "Themes" and droplet. Colours apply as you drag. Pass LivePreview = false to hold them back until the user hits Apply or Save.

The base theme follows the current one

Opening the editor starts from whatever theme is active, and changing the base applies it immediately, so you are always editing the theme you can see.

Designing a palette that works#

Start from a built-in that is close and change three things:

Luau
local base = Ember.Themes.Dark
 
local mine = {}
for role, colour in pairs(base) do mine[role] = colour end
 
mine.accent = Color3.fromRGB(255, 122, 47)
mine.accentInk = Color3.fromRGB(28, 12, 4)
mine.hover = Color3.fromRGB(38, 28, 24)
 
Ember.RegisterTheme("Mine", mine)

Three rules that stop a custom theme looking broken:

  1. accentInk must contrast with accent. It is text sitting on the accent colour. A pale accent needs a dark ink, and vice versa.
  2. bgpanelpanel2panel3 should get progressively lighter (or progressively darker on a light theme). Without those steps the window has no visible layers.
  3. faint still has to be readable. It paints descriptions and placeholders. If it disappears into panel2, every hint in your UI vanishes.