What is a theme?
What a theme changes, and what it does not.
A Recalbox theme dresses the interface: the list of your systems at startup, your game lists, the menus, the screensaver.
What a theme decides
- where elements are placed on screen, and how big;
- which images are shown: backgrounds, logos, frames, artwork;
- the colors and the fonts, including those of the menus;
- what shows up depending on the context: the screen resolution, the kind of machine, the highlighted game.
What a theme does not decide
- the content of the menus: Recalbox builds them itself. A theme only styles them;
- the games, their metadata, the emulators;
- what the buttons do.
The principle, in one sentence
Recalbox reads XML files that declare components — an image, a text, a list — each with its position, its size and its look.
<image name="fond">
<pos>0 0</pos>
<size>1 1</size>
<path>./data/fond.jpg</path>
</image>
The studio writes these files for you. This documentation explains the full syntax: it should let you write a theme entirely by hand if you wish.
The minimal working theme
The four views, in a single file.
A theme always nests the same way, and it is the outline of this whole documentation:
a theme contains views — one screen each; a view contains components — an image, a text, a list; a component carries properties — its position, its size, its color.
Here is a complete, working theme: all four views are there. Create a folder mon-theme/ in /recalbox/share/themes/, put this theme.xml inside, and it will appear in the list of themes.
<?xml version="1.0" encoding="UTF-8"?>
<theme name="My Theme" version="1.0" author="Me"
recalbox="10.0" compatibility="hdmi,crt" resolutions="hd,fhd">
<!-- ── 1. THE SYSTEM LIST ──────────────────────────────────── -->
<view name="system">
<box name="fond" extra="true">
<pos>0 0</pos><size>1 1</size>
<color>101820</color><zIndex>1</zIndex>
</box>
<carousel name="systemcarousel">
<type>horizontal</type>
<pos>0 0.35</pos><size>1 0.3</size>
<logoSize>0.2 0.12</logoSize>
<maxLogoCount>5</maxLogoCount>
</carousel>
<image name="logo"/>
<text name="systemInfo">
<pos>0.5 0.75</pos><origin>0.5 0.5</origin>
<fontSize>0.03</fontSize><color>8A92A6</color>
<alignment>center</alignment>
<backgroundColor>00000000</backgroundColor>
</text>
</view>
<!-- ── 2. THE GAME LIST ────────────────────────────────────── -->
<view name="detailed">
<box name="fond" extra="true">
<pos>0 0</pos><size>1 1</size>
<color>101820</color><zIndex>1</zIndex>
</box>
<textlist name="gamelist">
<pos>0.05 0.15</pos><size>0.42 0.75</size>
<primaryColor>C6CBD8</primaryColor>
<secondaryColor>8A92A6</secondaryColor>
<selectedColor>101820</selectedColor>
<selectorColor>4FE3C1</selectorColor>
<fontSize>0.035</fontSize>
</textlist>
<image name="md_image">
<pos>0.74 0.4</pos><origin>0.5 0.5</origin>
<maxSize>0.4 0.45</maxSize>
</image>
<text name="md_description">
<pos>0.54 0.68</pos><size>0.4 0.22</size>
<fontSize>0.024</fontSize><color>A0A8BA</color>
</text>
</view>
<!-- ── 3. MENU STYLING ─────────────────────────────────────── -->
<view name="menu">
<menuBackground>
<color>101820F0</color>
</menuBackground>
<menuText>
<fontSize>0.038</fontSize>
<color>C6CBD8</color>
<selectedColor>101820</selectedColor>
<selectorColor>4FE3C1</selectorColor>
</menuText>
</view>
<!-- ── 4. THE SCREENSAVER ──────────────────────────────────── -->
<view name="gameclip">
<extras>
<text name="titre">
<pos>0.06 0.86</pos>
<text>${game.name}</text>
<fontSize>0.05</fontSize><color>FFFFFF</color>
</text>
</extras>
</view>
</theme>
What to notice in it
extra="true"on the background: without it, it would not show at all. It is the rule to remember, explained in The most important rule;systemcarousel,logo,gamelist,md_imageare reserved names: that name — not the tag type — is what connects them to the engine. There are about thirty of them, and each view has its own: the full list is in The views and their components;${system}and${game.name}are variables: Recalbox replaces them at display time, with the name of the current system or of the highlighted game. That is what makes a theme feel alive without writing anything special — see Using variables;- the
menuview is not composed: you place nothing there, you style what Recalbox builds; - the
gameclipview is the screensaver — Recalbox plays game clips there, and the theme adds little more than information about the game being shown.
Everything fits in one file here, so it can be read at a glance. As soon as the theme grows, you split it up — see Splitting your theme.
Making your first theme
The way to go — with the studio, or by hand.
The result is the same: a theme folder. Two ways to get there.
With the studio
- Start from a template. A blank page is the worst starting point. A template comes with its components already in place: you replace them.
- Choose your screen resolutions — HD 16:9, CRT 4:3, vertical screen (TATE). What fits on a wide screen does not fit on a CRT television. Start with one, add the others later.
- Place your components: drag them from the left column, adjust them in the right one.
- Switch systems in View options and look: the most common mistake is a theme tuned on a single machine that breaks everywhere else. Logos do not all have the same shape, names do not all have the same length.
- Export, copy the folder to your machine, try it — see Trying your theme.
By hand
- Start from an existing theme. Open one, read it: it is the fastest way to understand how things are done. The official Recalbox theme is a good start.
- Create the folder and its
theme.xml— the only mandatory file. See Folders and files. - Announce what you target in the
<theme>tag:compatibilityfor the screen types,resolutionsfor the resolutions. These two attributes light up the pictograms of the theme manager, and announcing them wrong is promising what the theme does not deliver. See The header. - Write your views, one per screen, and split into several files as soon as it grows — see The minimal working theme, then Splitting your theme.
- Serve the other screens with conditions rather than copying everything:
<include if="crt">. - Copy the folder to your machine and try it — and read
themes.logat the first invisible component.
In both cases, the same mistake lies in wait: only checking on YOUR screen and YOUR system.
Folders and files
A single rule, and a lot of freedom.
A theme is a folder placed in /recalbox/share/themes/.
The only rule
This folder must contain a file named
theme.xml.
That is all. theme.xml is the entry point: Recalbox looks for it at the root of the folder, and if it is not there, the theme does not exist.
Nothing else is imposed. No subfolder names, no layout, no organization. You can write an entire theme in that single file.
Splitting, because it is more practical
A serious theme quickly grows to thousands of lines. So you split it into several XML files, arranged however you like, which theme.xml includes:
mon-theme/
theme.xml ← the only imposed name
variables.xml
views/
system.xml
detailed.xml
menu.xml
data/
fonts/
images/
Those names are yours: call them whatever you want. The custom, in published themes as in the studio's exports, is to write them in English — views/, data/, fonts/ — because that is the language of the tags they contain.
The include mechanism is explained in Splitting your theme.
Paths
A path written in a file is relative to that file. As soon as you split into subfolders, this becomes a source of errors.
Go through ${root}, which always points to the theme's root:
<path>${root}/data/images/fond.jpg</path> <!-- ✅ works from any file -->
<path>../data/images/fond.jpg</path> <!-- fragile: depends on where the line is written --> The header: the <theme> tag
What identifies your theme — and belongs ONLY in theme.xml.
<theme> is the root of every XML file in a theme. But its identity attributes only make sense in theme.xml: that is the file the theme manager reads to know what it is dealing with.
Remember In
theme.xml, fill them all in: without them, your theme shows up with no name, no version, and the manager does not know which screens it works on. In the other files, write a bare<theme>: repeating them brings nothing and can sow confusion.
The attributes
| Attribute | Role | Example | If missing |
|---|---|---|---|
name | Name shown in the theme list | name="My Theme" | the folder name |
version | Theme version | version="1.2" | not shown |
author | The author | author="Benoît" | not shown |
recalbox | Minimum Recalbox version required | recalbox="10.0" | all versions |
compatibility | Supported screen types: hdmi, crt, jamma, tate | compatibility="hdmi,crt" | hdmi |
resolutions | Supported resolutions: qvga, vga, hd, fhd | resolutions="hd,fhd" | fhd,hd |
compatibility and resolutions are exactly the pictograms shown by the theme manager: HDMI / CRT / JAMMA / TATE on one side, SD 240p / SD+ 480p / HD 720p / FULLHD 1080p on the other. Announcing them wrong is promising what the theme does not deliver.
⚠️ Without them, your theme cannot be published in the theme manager: the official repository requires a name, a version and an author. See Sharing your theme.
The two cases
In theme.xml — the full tag:
<theme name="My Theme" version="1.2" author="Benoît"
recalbox="10.0" compatibility="hdmi,crt" resolutions="hd,fhd">
…
</theme>
In every other file of the theme — the bare tag:
<theme>
<view name="system"> … </view>
</theme> Splitting your theme: <include>
A serious theme does not fit in a single file.
<include> loads another file at that exact spot, as if its content were copied there.
<include>${root}/views/system.xml</include>
<include path="${root}/views/detailed.xml" />
Both spellings work: the path as the tag's content, or as a path attribute.
Order matters
Recalbox reads top to bottom, and two components with the same name replace each other: the last one wins.
That is the whole overlay mechanism — and it is how theme options are built: each choice is a file loaded on top, redefining only what changes. See What an option is.
<include>${root}/views/base.xml</include> <!-- paints the background blue -->
<include>${root}/options/rouge.xml</include> <!-- repaints it red -->
Including under a condition
An <include> accepts if=, like a component: the file is only loaded if the condition is true.
<include if="crt">${root}/views/system-crt.xml</include>
That is how you serve a different layout per screen, without duplicating everything else. The list of conditions is in Conditional display.
Free components: extra="true" and <extras>
Why a component you add does not show up — and the two ways to declare it.
Two families of components
Within a view, Recalbox distinguishes:
- the elements it builds itself — the carousel, the game list, the help bar, the game's box art… They have a reserved name, and the theme merely adjusts them;
- the components you add — a text, an image, a color block, a video. These are the free components.
A free component must be declared as such
A free component placed directly in the view is not drawn: the machine only builds the elements marked extra. Two possible spellings, and they do exactly the same thing:
<image name="monLogo" extra="true">…</image>
<extras>
<image name="monLogo">…</image>
<text name="maMention">…</text>
</extras>
<extras> is a container: each of its children receives extra="true", in one go. It is shorter and more readable as soon as there are several components — it is what the studio writes.
What to remember
<extras>is not a component: it is not drawn, it is not positioned.- It does not nest in itself: an
<extras>inside an<extras>has no extra effect. - It works in the Systems view, the Game list and the Screensaver. ⚠️ Not in the Menu: the engine looks for no free component there. An
<extras>written for the Menu view is read without error… and never draws anything. - Elements with a reserved name stay outside: they already exist, you only adjust them.
In the studio, there is nothing to do: the components you add are automatically written inside
<extras>, and the reserved elements outside.
Your own variables: <variables>
What goes in the file: the tag, the naming rules, the scope.
This page describes what goes in the file. To create them without writing a line of XML, see Your custom variables, in “Dynamic data”.
It is the most useful mechanism of any serious theme, and yet the least known.
<variables>
<variable name="CouleurPrincipale" value="2E447C" />
<variable name="PoliceTitre" value="${root}/data/fonts/Exo2.otf" />
<variable name="Alpha50" value="80" />
</variables>
Then, anywhere in the theme:
<box name="fond">
<color>${CouleurPrincipale}</color>
</box>
<text name="titre">
<fontPath>${PoliceTitre}</fontPath>
<color>${CouleurPrincipale}${Alpha50}</color>
</text>
Change the value in one place, the whole theme follows. That is what makes color options possible: an overlay file redefines the variable, and nothing else.
The rules
nameandvalueare both mandatory; without either, the line is ignored and reported in the log;- an empty
nameis rejected; - a variable can contain another one, including a Recalbox variable:
<variable name="CheminLogo" value="${root}/data/logos/${system.name}.svg" />
- the
<variables>block accepts a condition, and each<variable>too:
<variables if="crt">
<variable name="TailleTitre" value="0.09" />
</variables>
- you can have several
<variables>blocks, in any file; - a variable redefined further down overwrites the previous one — like components.
⚠️ The block goes at the very top
<variables>applies to everything read AFTER it.
Recalbox replaces each ${nom} as it reads the files. A variable declared at the top of the theme therefore applies everywhere; declared in the middle, it only applies to what follows.
Hence the rule, valid for all themes: the variables block first, before the views, before everything else.
A variable redefined later replaces the previous one for the rest of the reading — that is what lets an option choice recolor a whole theme, provided it is loaded before the views. See Declaring an option.
Where to declare them
In theme.xml, before the <include> tags that use them: the engine reads a file's variables before processing its includes.
The custom, taken from the official theme, is to give them their own file — variables.xml — included first of all:
<include>${root}/variables.xml</include> The three ways to write a property
Tag, tag with value, or attribute — and why it changes everything.
The same property can be written in three ways. They are equivalent… except on one decisive point.
1. As a child tag
<image name="fond">
<path>./data/fond.jpg</path>
</image>
The historical form. The most readable as soon as a component has several properties.
2. As a child tag with a value attribute
<image name="fond">
<path value="./data/fond.jpg" />
</image>
Strictly equivalent to the first.
3. As an attribute of the component
<image name="fond" path="./data/fond.jpg" pos="0 0" size="1 1" />
Everything fits on one line. Very handy for simple components.
The difference that matters
Only forms 1 and 2 accept a condition on ONE property.
<image name="fond">
<path if="crt">./data/fond-crt.jpg</path>
<path if="!crt">./data/fond-hd.jpg</path>
</image>
Impossible in form 3: if= on the parent tag would condition the whole component, not one of its properties.
They mix freely
This is the important point: you do not have to pick one way and stick to it. Recalbox's reader accepts all three, including within the same component.
<image name="fond" pos="0 0" size="1 1" zIndex="1">
<path if="crt">${root}/images/fond-crt.jpg</path>
<path if="!crt">${root}/images/fond-hd.jpg</path>
<color value="FFFFFFC0" />
</image>
Three spellings in the same component: the simple values as attributes on the first line, the one with variants as conditional tags, and a last one as a value tag. It is perfectly valid, and it is even what you write in practice.
➡️ Simple rule: a single value → attribute; variants → child tag.
Ratio, percentage or pixels
The three accepted spellings for a position or a size.
Several properties are written with two numbers separated by a space: x y. This is the case for pos, size, maxSize, origin, rotationOrigin, logoSize and reflection.
Each number accepts three spellings, and you can mix them within the same pair.
| Spelling | Example | What it means |
|---|---|---|
| Ratio (the default) | 0.5 0.25 | a proportion of the screen, from 0 to 1 |
| Percentage | 50% 25% | the same thing, written differently |
| Pixels | 960p 270p | real pixels, p suffix |
<pos>0.5 0.5</pos> <!-- the center -->
<pos>50% 50%</pos> <!-- exactly the same -->
<pos>960p 540p</pos> <!-- the center… of a 1920×1080 screen only -->
Ratio or percentage: to be preferred
x is a proportion of the width, y of the height. A component at 0.5 0.5 is at the center of a 1920×1080 as of a 640×480. That is what lets a theme fit several screens.
Pixels: a fixed position
A pixel is a pixel. 960p is 960 pixels from the left edge, period.
It adapts to nothing: on a screen less than 960 pixels wide, the component is off screen. It is an absolute position, the opposite of the ratio.
Pixels are therefore only justified for what must stay fixed whatever the resolution: the thickness of a rule, an offset of a few points. For everything else, take the ratio.
The special case of fontSize
fontSize takes only one number, and its unit depends on its value:
| Value | What it means |
|---|---|
< 1 | a proportion of the screen's short side — 0.05 = 5% of the height in 16:9 |
>= 1 | a 240p reference size, which Recalbox multiplies for the screen |
Above 1, the value is not pixels. It is a size designed for a 240-line screen, multiplied one notch at each resolution step: ×1 up to a short side of 288 pixels, ×2 up to 576, ×4 at 1080p. <fontSize>8</fontSize> therefore makes 16 pixels on a 480p screen and 32 pixels at 1080p. The same rule applies everywhere — menu texts included.
<fontSize>0.045</fontSize> <!-- 4.5% of the short side: adapts everywhere, to be preferred -->
<fontSize>8</fontSize> <!-- “240p” size: 16 px at 480p, 32 px at 1080p --> Position, size, origin, rotation
Placing a component exactly where you want it.
pos and origin always go together
This is the point to understand before anything else, because the two do not talk about the same thing:
pos— where, on the SCREEN, the component is placed.origin— which point OF THE COMPONENT is placed at that spot.
One is a position on the screen, the other a point of the component. It is the meeting of the two that decides where the component appears.
pos — a position on the screen
<pos>0.1 0.2</pos>
These two numbers are measured from the screen's top-left corner: 0 0 is that corner, 1 1 the bottom-right corner.
The simplest is to read them as percentages — it is exactly the same thing: 0.1 = 10%, 0.2 = 20%, 0.5 = 50%. So pos 0.1 0.2 is 10% of the width and 20% of the height.
pos says nothing about the component itself: it is a mere point on the screen. What decides which part of the component lands there is origin.
origin — which point OF THE COMPONENT
origin answers the other half of the question: now that we know where on the screen, which part of the component comes to rest there?
These nine values are the most common, but any pair between 0 and 1 works:
By default, origin is 0 0 — the component's top-left corner. That is why a component without origin extends to the right and downward from its pos.
The example that makes it all click
Take a landscape image on a 16:9 screen:
<pos>0.5 0.5</pos>
<origin>1 1</origin>
pos 0.5 0.5 is the center of the screen. You might think the image will be centered there. It is not: origin 1 1 designates the image's bottom-right corner, and it is that corner which is placed at the center.
The image therefore ends up entirely above and to the left of the center.
To really center
<pos>0.5 0.5</pos>
<origin>0.5 0.5</origin>
This time it is the image's center that is placed at the screen's center.
The full table
origin | The component's point placed on pos |
|---|---|
0 0 | top-left corner — the default value |
0.5 0 | middle of the top edge |
1 0 | top-right corner |
0 0.5 | middle of the left edge |
0.5 0.5 | the center |
1 0.5 | middle of the right edge |
0 1 | bottom-left corner |
0.5 1 | middle of the bottom edge |
1 1 | bottom-right corner |
What it is really for
Without origin, every component is placed by its top-left corner: to center something, you would have to compute 0.5 − width/2, and recompute every time the width changes.
With origin, you compute nothing anymore:
| What you want | pos | origin |
|---|---|---|
| centered on screen | 0.5 0.5 | 0.5 0.5 |
| stuck to the right edge | 1 … | 1 … |
| stuck to the bottom edge | … 1 | … 1 |
| centered at the bottom | 0.5 1 | 0.5 1 |
It is especially useful for a system logo, whose width changes from one machine to the next: with origin, it stays aligned no matter what.
size — the imposed size
<size>0.3 0.2</size>
The component is exactly that size. For an image, this means it is distorted to fill the box.
Give only one
Put 0 for the other: the missing dimension is computed to keep the proportions.
<size>0.3 0</size> <!-- 30% wide, proportional height -->
⚠️ This is not the same as keepratio or maxSize, even though both keep proportions:
| Spelling | What is guaranteed |
|---|---|
size 0.3 0 | the width is exactly 30%; the height follows, whatever it is — it may overflow |
maxSize 0.3 0.2 | the image fits in the box: the most constraining dimension wins, so the width may be reduced |
In other words: size with a zero imposes one constraint, maxSize imposes two.
maxSize — the maximum size, without distortion
<maxSize>0.3 0.2</maxSize>
The image is enlarged or reduced while keeping its proportions to fit in the box. It therefore rarely fills its whole surface.
For a system logo,
maxSizeis almost always what you want. Logos do not have the same shape from one machine to the next: withsize, some would be squashed.
size together with keepratio gives the same result as maxSize — and it is often the preferred spelling, because it states the intended size instead of a limit:
<size>0.3 0.2</size>
<keepratio>true</keepratio> <!-- ⚠️ all lowercase -->
maxSize only exists on image and video.
rotation and rotationOrigin
<rotation>90</rotation>
<rotationOrigin>0.5 0.5</rotationOrigin>
rotation is in degrees, clockwise. rotationOrigin designates the point around which the component pivots, in its own proportions. Without rotationOrigin, the pivot is the top-left corner (0 0) — write 0.5 0.5 to rotate around the center.
Rotation applies to images, color blocks, videos and Markdown blocks. A rotated text draws at small angles but disappears at 90°; a rotated scrolling text only draws its background, never its text. For a vertical title, go through an image or a Markdown block.
Depth (
zIndex) and switching off (disabled) have their own page: Depth and visibility.
Depth and visibility
What goes in front of what, and how to switch a component off.
Two properties every component accepts, which talk about neither position nor size: which one goes in front of the other, and which one does not show at all.
zIndex — the layers
<zIndex>40</zIndex>
It is a layer system, exactly like in a drawing application: each component is a sheet, and zIndex says in which order they are stacked. The bigger the number, the closer to you the component is.
Reserved components have their default values, and free components receive 10 if you say nothing. Leave room between yours — 10, 20, 30 — so you can slip one in between later.
disabled — switching a component off
<disabled>true</disabled>
The component is read but not displayed. Handy to hide a reserved component you do not want, without having to redefine it entirely.
⚠️ Neither carousel nor textlist accepts it.
Colors and gradients
RRGGBB, transparency, and the eight corners.
The spelling
A color is written in hexadecimal, without a hash:
<color>2E447C</color> <!-- opaque -->
<color>2E447C80</color> <!-- half transparent -->
- 6 characters:
RRGGBB, opaque; - 8 characters:
RRGGBBAA, the last two give the opacity —00invisible,80half,FFopaque.
The most useful transparency values
Everyone knows a color's code. The two opacity characters, much less:
| Opacity | To write | Opacity | To write |
|---|---|---|---|
| 0% — invisible | 00 | 60% | 99 |
| 10% | 1A | 70% | B3 |
| 20% | 33 | 75% | BF |
| 25% | 40 | 80% | CC |
| 30% | 4D | 90% | E6 |
| 40% | 66 | 95% | F2 |
| 50% | 80 | 100% — opaque | FF |
The math, if your value is not there: the percentage × 255, written in hexadecimal.
Gradients
box and image accept one color per edge or per corner. Giving two is enough to get a gradient.
| Property | Effect |
|---|---|
colorTop + colorBottom | vertical gradient |
colorLeft + colorRight | horizontal gradient |
colorTopLeft, colorTopRight, colorBottomLeft, colorBottomRight | four-corner gradient |
<box name="ombre-du-haut">
<pos>0 0</pos>
<size>1 0.3</size>
<colorTop>00000080</colorTop>
<colorBottom>00000000</colorBottom>
</box>
A gradient from half-transparent black to fully transparent: the classic veil under which a title stays readable whatever the image behind.
Tinting an image
On an image component, color does not fill: it multiplies the image. A white image therefore takes exactly the given color — that is how you recolor an icon without redoing the file.
<image name="etoile">
<path>${root}/data/arts/etoile-blanche.svg</path>
<color>FFC24B</color> <!-- the star turns golden -->
</image> Conditioning, translating, regionalizing
A different value depending on the screen, the machine or the language.
A condition on a property
<text name="titre">
<fontSize if="crt">0.09</fontSize>
<fontSize if="!crt">0.05</fontSize>
<color>FFFFFF</color>
</text>
One component, two sizes depending on the screen. The list of conditions is on the Conditional display page.
A condition on the whole component
<image name="filtre" if="crt">
<path>${root}/data/arts/scanlines.png</path>
</image>
The component only exists if the condition is true. The full list is in Conditional display, and Combining several conditions explains and, or and parentheses.
ifexists and ifnotexists
These two do not test the machine but the presence of a file:
<image name="jaquette">
<path ifexists="${game.media.imagepath}">${game.media.imagepath}</path>
<path ifnotexists="${game.media.imagepath}">${root}/images/pas-dimage.png</path>
</image>
It is the clean way to handle games without box art — otherwise the slot stays empty.
Translating a text
A language suffix on the property is enough:
<text name="bienvenue">
<text>Bienvenue</text>
<text.en>Welcome</text.en>
<text.es>Bienvenido</text.es>
</text>
Recalbox takes the matching variant, and the suffix-free version if none matches.
What a suffix accepts
| Suffix | What it targets |
|---|---|
.fr .es .de | the machine's language, in lowercase |
.fr_FR | the language and the country |
.US .EU .JP | the region, in UPPERCASE |
The region
This is what lets you write “Genesis” in the United States and “Mega Drive” in Europe. Same mechanism as the language, with an uppercase suffix — it is what Recalbox's official theme uses:
<box name="fond"
color.US="2E447C"
color.EU="7C2E44"
color.JP="447C2E" />
The three values are US, EU and JP. A property without a suffix applies to all regions.
⚠️ There is always an active region. On a fresh machine it is US: what you write without a suffix is what most people will see, and .EU or .JP only serve to depart from it.
The region is changed on the machine, in the interface settings — and Recalbox then re-reads the whole theme, as for an option. The studio offers the same choice, to see what each audience will see.
What it works on: EVERYTHING
There is no list of translatable properties. The suffix is examined on every property before the engine even knows which one it is — so all of them accept it:
<text name="titre" text.fr="Bienvenue" text.es="Bienvenido" />
<image name="logo" path.US="genesis.png" path.EU="megadrive.png" />
<text name="mention" fontSize.de="0.03" /> <!-- German runs longer -->
<box name="bandeau" color.JP="D62828" />
Both spellings support it — as an attribute (size.fr="…") as well as a sub-node (<size.fr>…</size.fr>) — and even the tag name (<text.fr name="…">). It also applies to an option's title and help.
⚠️ The localized version wins for good. Once a variant has been applied, the suffix-free version of the same property is refused, including in a file read later. An overlay can therefore not “take back” a property already localized: it must provide its own localized variant.
Loading a text from a file
text, scrolltext and markdown accept path instead of text: the content is then read from the file. Handy for a long presentation text.
<markdown name="apropos">
<path>${root}/data/textes/apropos.md</path>
</markdown> Naming and reusing
A component's name, and how to create several at once.
Two things not to confuse
<image name="mon-fond">
imageis the component type: it decides what the component can do and which properties it accepts. It must be picked from the list of existing types — see The components. An invented type is ignored;nameis your label. It is free… unless you reuse one of the names reserved by Recalbox, in which case the component is wired to the engine — see The views and their components.
Two components with the same name in the same view are one: the second completes or replaces the first. It is intended — it is the overlay mechanism — but it is a source of errors when you name two backdrops “fond” without thinking.
Creating several components at once
<box name="bande1, bande2, bande3, bande4">
<size>0.01 1</size>
<color>FFFFFF20</color>
</box>
Four components, same properties. You still have to give each its own position further down:
<box name="bande1"><pos>0.90 0</pos></box>
<box name="bande2"><pos>0.92 0</pos></box>
Modifying without rewriting everything
Since the last one wins, it is enough to redeclare the single property that changes:
<include>${root}/views/base.xml</include>
<view name="system">
<box name="fond"><color>7C2E44</color></box> <!-- the rest is kept -->
</view> All the components
What exists, and what each one can do.
A component is declared by its type — the tag name. A type not in this list is ignored by Recalbox and reported in the log.
Displaying something
| Type | What it does |
|---|---|
text | a text, on one or several lines |
scrolltext | a text that scrolls when too long |
markdown | a formatted text (bold, headings, lists) |
image | an image |
video | a video |
box | a color block, or a gradient |
datetime | a date |
rating | a rating, as stars |
sound | a sound (nothing displayed) |
Lists and navigation
| Type | What it does |
|---|---|
textlist | the game list |
carousel | the system carousel |
helpsystem | the help bar at the bottom of the screen |
Menu styling
These nine are not placed: they adjust the look of the menus Recalbox builds itself.
menuBackground · menuIcons · menuText · menuTextSmall · menuSection · menuSwitch · menuSlider · menuButton · menuSize
Virtual keyboard styling
keyboard is not placed either: it gives its colors and its font to the keyboard Recalbox opens for a search or an input. It is set up in Global components.
Mind the case
Names are case sensitive. menuswitch does not work, it must be menuSwitch. A single property is the exception, written all lowercase: keepratio.
text, scrolltext, markdown
The three ways to display text.
text — regular text (18 properties)
<text name="titre">
<pos>0.06 0.08</pos>
<size>0.5 0.1</size>
<text>${system}</text>
<fontPath>${root}/data/fonts/Exo2.otf</fontPath>
<fontSize>0.05</fontSize>
<color>FFFFFF</color>
<alignment>left</alignment>
<forceUppercase>true</forceUppercase>
</text>
| Property | Type | Role |
|---|---|---|
pos size origin rotation rotationOrigin | pair | placement — see Position, size, origin |
text | text | the content, variables included |
path | path | reads the content from a file, instead of text |
fontPath | path | the font |
fontSize | number | < 1 = screen-height ratio, >= 1 = pixels |
fontStyle | text | normal, bold, italic, bolditalic |
color | color | the text color |
backgroundColor | color | a background behind the text |
alignment | text | see below |
forceUppercase | yes/no | all capitals |
lineSpacing | number | line spacing, 1.2 by default |
multiline | yes/no | allow line breaks |
zIndex | number | depth |
disabled | yes/no | switch the component off |
alignment acts on two axes
The value combines the horizontal and the vertical.
Nine positions, thirteen ways to write them — four values are synonyms:
| Position | To write | Synonym |
|---|---|---|
| top-left | topleft | |
| top-center | topcenter | top |
| top-right | topright | |
| center-left | centerleft | left |
| center | center | |
| center-right | centerright | right |
| bottom-left | bottomleft | |
| bottom-center | bottomcenter | bottom |
| bottom-right | bottomright |
⚠️ An unknown value does not keep the previous alignment: it falls back to center-left, the default value.
The text is aligned within its size box: without size, the alignment has nothing to act on.
scrolltext — scrolling text (16 properties)
Same properties as text, without multiline and lineSpacing. The text scrolls horizontally when it overflows its box.
<scrolltext name="titre-long">
<size>0.4 0.06</size>
<text>${game.name}</text>
<fontSize>0.04</fontSize>
</scrolltext>
To be reserved for values whose length you do not control — a game name, a developer.
markdown — formatted text (15 properties)
<markdown name="synopsis">
<pos>0.06 0.5</pos>
<size>0.4 0.35</size>
<text>${game.synopsis}</text>
<fontSize>0.028</fontSize>
<color>C6CBD8</color>
</markdown>
Same properties as text, without fontStyle, backgroundColor or multiline.
It understands simple formatting in the text: **bold**, *italic*, # heading, dashed lists. Useful for a synopsis or an “about” page.
⚠️
markdowndoes not scroll: a text longer than its box is cut off.
image and video
Displaying an image or a video, and tinting them.
image (21 properties)
<image name="jaquette">
<pos>0.75 0.4</pos>
<origin>0.5 0.5</origin>
<maxSize>0.35 0.4</maxSize>
<path>${game.media.imagepath}</path>
<zIndex>30</zIndex>
</image>
| Property | Type | Role |
|---|---|---|
pos size origin rotation rotationOrigin | pair | placement |
maxSize | pair | maximum size without distortion — see Position, size |
keepratio | yes/no | keep the proportions (⚠️ all lowercase) |
path | path | the image file |
tile | yes/no | repeat the image as tiles instead of stretching it |
color | color | tints the image (multiplication) |
colorTop colorBottom colorLeft colorRight | color | gradient tint |
colorTopLeft colorTopRight colorBottomLeft colorBottomRight | color | four-corner tint |
reflection | pair | a reflection under the image: start and end opacity |
zIndex disabled | depth, switching off |
Formats
PNG, JPG and SVG. SVG is recommended for logos: it stays sharp at every size.
tile — the repeated texture
<image name="grille">
<size>1 1</size>
<path>${root}/data/arts/motif.png</path>
<tile>true</tile>
</image>
The image keeps its original size and repeats to cover the box.
reflection
<reflection>0.5 0.0</reflection>
Adds a flipped reflection under the image, from 50% opacity at the top to 0% at the bottom.
video (15 properties)
<video name="md_video">
<pos>0.75 0.4</pos>
<origin>0.5 0.5</origin>
<maxSize>0.35 0.3</maxSize>
<delay>1.5</delay>
<loops>0</loops>
</video>
Same placement properties as image, plus:
| Property | Type | Role |
|---|---|---|
delay | number | seconds before the video starts |
loops | number | number of plays; 0 = loop |
animations | text | the appearance effect |
link | text | tie the playback to another component |
reflection | pair | reflection, as on image |
video accepts neither tile nor the tint colors.
A
delayof one or two seconds prevents the video from firing on every game merely passed through while scrolling the list.
box — the color block
Backgrounds, veils, bands and gradients.
The simplest component, and one of the most useful: backgrounds, veils, bands, separators. 16 properties.
<box name="fond" extra="true">
<pos>0 0</pos>
<size>1 1</size>
<color>101820</color>
<zIndex>1</zIndex>
</box>
| Property | Role |
|---|---|
pos size origin rotation rotationOrigin | placement |
color | solid color |
colorTop colorBottom | vertical gradient |
colorLeft colorRight | horizontal gradient |
colorTopLeft colorTopRight colorBottomLeft colorBottomRight | four-corner gradient |
zIndex disabled | depth, switching off |
No path: a box displays no image. For an image background, take image.
The readability veil
The most common use case: making a text readable over any image.
<box name="voile-du-bas" extra="true">
<pos>0 0.7</pos>
<size>1 0.3</size>
<colorTop>00000000</colorTop>
<colorBottom>000000C0</colorBottom>
<zIndex>20</zIndex>
</box>
From transparent to 75% black: the bottom of the screen darkens progressively, and the text placed on top stays readable whatever the artwork behind.
A thin band
<box name="filet" extra="true">
<pos>0.06 0.18</pos>
<size>0.3 2p</size>
<color>FFFFFF40</color>
</box>
2p = two pixels tall, whatever the resolution: one of the rare cases where the pixel unit is the right choice.
textlist — the game list
The list where you pick your game: its colors, its highlighter, its font.
⚠️ This list is not placed wherever you want.
textlistonly exists in the Games view, under the reserved namegamelist. Elsewhere — or under another name — it is not built, and you cannot add a second one.
The properties (19)
<textlist name="gamelist">
<pos>0.05 0.2</pos>
<size>0.4 0.7</size>
<primaryColor>C6CBD8</primaryColor>
<secondaryColor>8A92A6</secondaryColor>
<selectedColor>101820</selectedColor>
<selectorColor>4FE3C1</selectorColor>
<selectorHeight>0.055</selectorHeight>
<fontSize>0.035</fontSize>
<horizontalMargin>0.01</horizontalMargin>
</textlist>
| Property | Role |
|---|---|
pos size origin | placement |
primaryColor | the color of the games |
secondaryColor | the color of the folders |
selectedColor | the text color of the chosen line |
selectorColor | the color of the highlighter |
selectorImagePath | a highlighter image, instead of the color |
selectorImageTile | repeat that image as tiles |
selectorHeight | the highlighter's height |
selectorOffsetY | its vertical offset |
fontPath fontSize | the font |
alignment | line alignment |
horizontalMargin | the left and right margin |
forceUppercase | all capitals |
lineSpacing | line spacing — this is what spaces the lines |
scrollSound | the sound played while scrolling |
zIndex | depth |
⚠️ No disabled: a game list does not switch off.
primaryColor and secondaryColor are the most common source of confusion: the second is not the alternate color of every other line, it is the color of the folders.
carousel — the system carousel
The strip that scrolls the systems: direction, logo size, and the text mode.
The carousel displays the systems, logo by logo. It only exists in the Systems view, under the reserved name systemcarousel, and the engine builds only one. Elsewhere, or under another name, it is not built at all.
The scrolling direction
type accepts horizontal (the default), vertical and vertical_wheel — the wheel. There is no horizontal wheel: any other value silently falls back to horizontal.
The properties (28)
<carousel name="systemcarousel">
<type>vertical</type>
<pos>0 0</pos>
<size>0.25 1</size>
<color>00000020</color>
<logoSize>0.12 0.075</logoSize>
<logoScale>1.5</logoScale>
<maxLogoCount>7</maxLogoCount>
<logoAlignment>center</logoAlignment>
<defaultTransition>instant</defaultTransition>
</carousel>
| Property | Role |
|---|---|
type | horizontal, vertical, vertical_wheel |
pos size origin | placement |
color | the carousel's background |
logoSize | the size of a logo (pair) |
logoScale | the enlargement of the chosen logo |
logoRotation logoRotationOrigin | logo rotation (wheels) |
logoAlignment | logo alignment within their slot |
maxLogoCount | how many logos visible at once |
defaultTransition | fade or instant; any other value gives slide |
fontPath fontSize fontColor | the text mode's font |
forceUppercase | system names all in capitals |
textOnly | write the names instead of the logos |
primaryColor secondaryColor | the color of the names |
selectedColor | the color of the chosen name |
selectorColor selectorHeight | the text mode's highlighter |
selectorOffsetX selectorOffsetY | its offset |
textOffsetX | the text offset |
lineSpacing horizontalMargin | line spacing and margins of the text mode |
zIndex | depth |
⚠️ No disabled: the carousel does not switch off.
⚠️ maxLogoCount is not a number of displayed logos. It only drives spacing and centering: the engine draws more of them, which overflow and get clipped. And the value is rounded to the integer — a decimal (2.5) is therefore useless.
Logos, or names
In the System list section, “WHAT SCROLLS BY” picks between:
- Logos — the usual behavior;
- Names — the carousel writes each system's name.
⚠️ It is not a fourth scrolling direction: the text mode combines with horizontal, vertical and wheel. A carousel of names can therefore scroll in any direction.
The properties that go with it
In text mode, the font, its size and its color are set in the Text section. Two offsets specific to the carousel are added:
- Highlighter horizontal offset (
selectorOffsetX); - Text horizontal offset (
textOffsetX).
What it writes
<carousel name="systemcarousel" type="vertical">
<textOnly>true</textOnly>
<textOffsetX>0.02</textOffsetX>
</carousel>
Available from Recalbox 10.1. On an older machine,
textOnlyis ignored and the carousel shows the logos.
rating — the star rating
The game's rating, drawn as stars: the two images it is made of.
The properties (9)
<rating name="md_rating">
<pos>0.06 0.72</pos>
<size>0.12 0.024</size>
<filledPath>${root}/data/arts/etoile-pleine.svg</filledPath>
<unfilledPath>${root}/data/arts/etoile-vide.svg</unfilledPath>
</rating>
| Property | Role |
|---|---|
pos size origin rotation rotationOrigin | placement |
filledPath | the filled star image |
unfilledPath | the empty star image |
zIndex disabled | depth, switching off |
size designates the whole of the five stars. A width of five times the height gives square stars.
No color: to change the tint, change the images — or provide white images and tint them… which rating does not allow. Two files are therefore necessary.
datetime — a date
A game's release date, or that of the last play.
A datetime displays a date Recalbox knows, never free text. Its name says which date — md_releasedate the game's release, md_lastplayed the last play — and its display property says in which form to write it.
<datetime name="md_releasedate">
<pos>0.06 0.66</pos>
<fontSize>0.028</fontSize>
<color>C6CBD8</color>
<display>date</display>
</datetime>
display — the date's form
| To write | What shows |
|---|---|
date | 1991/06/23 |
dateTime | 1991/06/23 14:05:30 |
year | 1991 |
time | 14:05:30 |
realTime | the current time — not a game date |
RelativeToNow | “3 days ago” |
⚠️ Case matters. datetime does not work, it must be dateTime; neither does relativeToNow, it must be RelativeToNow. An unknown value keeps the previous form and lands in themes.log.
The properties (12)
| Property | Role |
|---|---|
pos size origin | placement |
display | the date's form |
color backgroundColor | colors |
fontPath fontSize | font |
alignment forceUppercase | formatting |
zIndex disabled | depth, switching off |
⚠️ A datetime without color is invisible: it is the only component that resets the color to zero instead of keeping the previous one. And of alignment, only the horizontal part is kept — the vertical is always centered.
A game without a date shows an empty line: it is the data that is missing, not the theme.
sound — the theme's music
One track, or a folder of tracks, played while browsing.
A sound is not drawn: it has neither position, nor size, nor depth. It carries a path, and nothing else.
Two names, two behaviors:
| Name | What the machine does |
|---|---|
bgsound | plays that track |
directory | picks at random from that folder |
⚠️ The machine only reads it in the Systems view. Placed elsewhere, it never plays. On the other hand, a system file can redefine it: that is how you give one music per system.
The user's own music, if any, takes precedence over the theme's.
helpsystem — the help bar
The 32 button icons, one by one.
The bar at the bottom of the screen that reminds you what the buttons do. 38 properties: six for formatting, and 32 icons.
Recalbox itself decides what the bar announces, screen by screen, and translates each label. Your theme only sets the form: the placement, the font, the colors, and the image of each pictogram.
<helpsystem name="help">
<pos>0.02 0.955</pos>
<fontPath>${root}/data/fonts/Exo2.otf</fontPath>
<fontSize>0.025</fontSize>
<textColor>C6CBD8</textColor>
<iconColor>FFFFFF</iconColor>
<iconA>${root}/data/arts/boutons/a.svg</iconA>
<iconB>${root}/data/arts/boutons/b.svg</iconB>
</helpsystem>
Formatting
| Property | Role |
|---|---|
pos size | placement |
textColor | the labels' color |
iconColor | the icons' tint — provide them in white |
fontPath fontSize | the font |
The 32 icons
Replacing an icon is optional: Recalbox provides its own. You only redefine the ones you want.
Directions — iconUpDown, iconLeftRight, iconUpDownLeftRight
Buttons — iconA, iconB, iconX, iconY
Triggers — iconL, iconR, iconL2, iconR2, iconL3, iconR3, iconLR, iconL2R2, iconL3R3
System — iconStart, iconSelect, iconHotkey
Hotkey combinations — iconHkA, iconHkB, iconHkX, iconHkY, iconHkL, iconHkR, iconHkLeftRight
Joysticks — iconJ1UpDown, iconJ1LeftRight, iconJ1UpDownLeftRight, iconJ2UpDown, iconJ2LeftRight, iconJ2UpDownLeftRight
A combination is two pictograms
When the help is about a combination, Recalbox draws iconHotkey, then the key.
[HK] [A] Launch the game
iconHkA is therefore the image of the A button used in a combination — not a drawing of “HK + A”. Provide a plain button there: the shortcut is already in front.
iconHotkeycounts double: it appears in front of every combination in the bar. It is the icon to polish first if your theme shows any.
The name is imposed: help
Recalbox looks for the component named help, and it alone. <helpsystem name="barre"> will exist in your file without ever driving anything.
The bar is set view by view
Recalbox re-reads the view's <helpsystem> at every screen change, and starts over from its own icons. What a view does not declare therefore falls back to Recalbox's default — never to what another view had set.
Two ways to do it:
- the same set everywhere — declare the bar in a view that covers everything:
<view name="system, detailed, menu">; - a different bar per screen — a
<helpsystem>in each view, with its own images.
Provide white icons and use
iconColor: a single set of files is then enough for all your theme's color variants.
Offering several icon sets
That is what the big themes do: a SNES set, an Xbox set, a PlayStation set… and the user picks in Menu → Interface → Theme.
Each set is a file containing only the <helpsystem> and its images:
<!-- ./options/icones-snes.xml -->
<theme>
<view name="system, detailed, menu">
<helpsystem name="help">
<iconA>${root}/data/icones/snes/a.svg</iconA>
<iconB>${root}/data/icones/snes/b.svg</iconB>
</helpsystem>
</view>
</theme>
And you offer them like this, without declaring anything else:
<include subset="iconset" name="1 - SNES">${root}/options/icones-snes.xml</include>
<include subset="iconset" name="2 - Xbox">${root}/options/icones-xbox.xml</include>
iconsetis a reserved name: Recalbox displays “SELECT THEME'S ICONSET”, translated into the machine's language. No<subset>tag is needed to name it.
Menu styling
The nine components that adjust Recalbox's menus.
Recalbox builds its menus itself: their content is not yours. The theme only adjusts their look, through nine components that are not placed.
menuBackground (3)
<menuBackground>
<color>101820F0</color>
<path>${root}/data/arts/cadre-menu.png</path>
<fadePath>${root}/data/arts/voile.png</fadePath>
</menuBackground>
| Property | Role |
|---|---|
color | the frame's color — it tints the image if path is given |
path | the frame's image |
fadePath | the veil image that darkens the view behind |
menuText (6) — the menu lines
| Property | Role |
|---|---|
fontPath fontSize | the font |
color | the lines' text |
selectedColor | the chosen line's text |
selectorColor | the highlighter |
separatorColor | the rules between lines |
menuTextSmall (5) — the small text
fontPath, fontSize, color, selectedColor, selectorColor. Used for setting values and drop-down lists.
menuSection (5) — the section headers
fontPath, fontSize, color, selectedColor, alignment.
menuSize (1)
<menuSize><height>0.85</height></menuSize>
The maximum height of the menu frame, as a screen proportion. It is the only measurement a theme imposes on the menus.
menuSwitch (2) — the switches
pathOn, pathOff — the two images of a yes/no setting.
menuSlider (1) — the sliders
path — the cursor's image.
menuButton (2) — the buttons
path, filledPath — the normal and the pressed state.
menuIcons (23) — the section icons
One icon per menu section:
iconSystem · iconUpdates · iconThemes · iconGames · iconUI · iconTate · iconControllers · iconSound · iconNetwork · iconScraper · iconBios · iconDownload · iconLicense · iconAdvanced · iconArcade · iconKodi · iconCardReader · iconRecalboxRGBDual · iconQuit · iconRestart · iconShutdown · iconFastShutdown · iconList
⚠️
iconListis singular, unlike the “Lists” section it represents. In the plural, it is ignored.
Virtual keyboard styling
The colors and the font of the keyboard that opens to search a game or type a text.
Like the menus, it is not placed: the keyboard decides its own geometry, the theme only picks its colors and its font.
It opens over the screen you are on, whichever it is — so it is set once, in Global components.
| Property | What it paints |
|---|---|
keyColor | each key's background at rest |
keySelectedColor | the key you are on |
keyTextColor | the letter written on the key |
keyDisabledColor | the letter of a character the input refuses |
keyModifierColor | Shift / Ctrl / Alt pressed for a single key |
keyModifierLockedColor | Shift / Ctrl / Alt locked |
keyTitleColor | the title above the keyboard |
keyEditTextColor | the text being typed |
fontPath | the font — the size stays decided by the keyboard |
Three keyboards, and the user chooses
The theme does not decide which one shows: it is a console setting. And they do not all read your colors.
- Arcade wheel — only four: hovered key, letter, title, typed text. Neither key background nor font.
- Classic keyboard — all eight, plus the font.
- Simplified keyboard — all eight except the two Shift / Ctrl / Alt colors (armed and locked): this keyboard has no such keys.
So set first the four that all of them read: your styling will hold whatever the keyboard.
What it writes
<view name="system, basic, detailed, menu, gameclip">
<keyboard name="keyboard">
<keyColor>1B1D22</keyColor>
<keySelectedColor>4FE3C1</keySelectedColor>
<keyTextColor>FFFFFF</keyTextColor>
<keyTitleColor>FFFFFF</keyTitleColor>
<keyEditTextColor>FFFFFF</keyEditTextColor>
</keyboard>
</view>
The name must be exactly keyboard, and it accepts neither pos nor size.
Available from Recalbox 10.1. On an older machine, the block is ignored and the keyboard keeps its factory colors.
⚠️ The most important rule
Why your component does not show up.
This is the cause of “I placed my image and I can't see it”.
A view only draws two things:
- its reserved components, which it builds itself;
- the components marked
extra="true".A free component without
extranever shows up.
<view name="system">
<image name="mon-decor" extra="true"> <!-- ✅ shows -->
<path>${root}/data/arts/decor.png</path>
</image>
<image name="autre-decor"> <!-- ❌ invisible -->
<path>${root}/data/arts/decor.png</path>
</image>
</view>
The other, equivalent way is to group them:
<view name="system">
<extras>
<image name="mon-decor"> … </image>
<text name="ma-legende"> … </text>
</extras>
</view>
Only SIX types can be placed freely
The free-component factory only knows how to build these:
✅ Usable as extra | ❌ Refused |
|---|---|
image · box · video · text · scrolltext · markdown | textlist · carousel · datetime · rating · sound · helpsystem · keyboard · container · ninepatch · all the menu* |
A refused type writes Extra type unknown: Rating to the log and nothing is drawn.
➡️ rating, datetime, textlist and carousel are only used under their reserved name, in a view that expects them. You cannot add a second rating or a second list.
➡️ ninepatch has no reserved name anywhere: refused as a free component, and no view builds it. It is therefore unusable in practice, despite being present in the engine.
➡️ container is not placed either, but it is not useless: the engine applies it on top of another component. In the game list, md_description declared as a <text> also sets the scrolling frame around it — pos, size and zIndex go to the frame, the rest to the text. You never write it yourself.
Three other limits
- only one
<video>per view — the second is ignored without a message; - an extra gets a
zIndexof 10 by default; - extras are sorted by
zIndexbefore display.
The exception
helpsystem keeps working even written inside an <extras> block: the view finds it back by its name. The log warning has no consequence.
The system view — the machines
The carousel, the logo, the information line.
The first screen: the list of your machines.
The reserved components
| Name | Type | Role |
|---|---|---|
systemcarousel | carousel | the carousel — one only, not renamable |
logo | image | the system's logo in the carousel |
systemInfo | text | the “510 games available, 13 favorites” line |
bgsound | sound | the theme's background music |
directory | sound | the theme's music folder |
bgsoundanddirectoryare the only names<sound>accepts, and only in this view.
Two traps
systemInfo has a gray background by default. To remove it:
<text name="systemInfo">
<backgroundColor>00000000</backgroundColor>
</text>
The carousel is unique. A second <carousel> is read without error but never drawn.
The default logo
When the theme does not provide a logo, Recalbox looks for its own in this order:
<system>-<language_COUNTRY>.svg → <system>-<language>.svg
→ <system>-<region>.svg → <system>.svg
That is what allows a “Genesis” logo in the United States and “Mega Drive” in Europe without writing anything.
The carousel in detail
| Property | Default | Detail |
|---|---|---|
type | horizontal | horizontal, vertical, vertical_wheel; any other value falls back to horizontal |
logoSize | computed | ratio of the screen, not of the carousel |
logoScale | 1.2 | enlargement of the chosen logo |
maxLogoCount | 3 | rounded to the integer — a decimal is useless |
color | transparent | the carousel's background |
The spacing of the logos is computed like this:
spacing = (length − logoSize × maxLogoCount) / maxLogoCount + logoSize
where length is size.y in vertical, size.x in horizontal.
The detailed view — the games
The list, the game's sheet, and its thirty reserved components.
The list of a system's games, with the sheet of the hovered game.
This is the view of all game lists: the
basicview is never requested by the engine, and the arcade view reusesdetailed.
The list and the media
| Name | Type | Role |
|---|---|---|
gamelist | textlist | the game list |
logo | image | the system's logo |
md_image | image | the cover art |
default_image_path | image | the fallback image when the game has no cover |
md_video | video | the preview video |
md_region1 … md_region4 | image | the game's four region flags |
On
md_image, thepathwritten in the theme is ignored: the image comes from the game. On themd_region*, onlypos,size,zIndexandpathare read.
The game's information
| Name | Type |
|---|---|
md_description | text, markdown or scrolltext — your choice |
md_folder_name | text |
md_rating | rating |
md_releasedate, md_lastplayed | datetime |
md_developer, md_publisher, md_genre, md_players, md_playcount, md_favorite | text |
The labels
md_lbl_rating, md_lbl_releasedate, md_lbl_developer, md_lbl_publisher, md_lbl_genre, md_lbl_players, md_lbl_lastplayed, md_lbl_playcount, md_lbl_favorite
⚠️ Their text is imposed. Recalbox writes “Rating:”, “Released:”, “Developer:”… translated into the machine's language, after applying the theme. A
text=in the theme is overwritten. You set their formatting, never their content.Likewise, the
md_*values accept every property excepttext: their content comes from the game.
The list's colors
gamelist uses five colors, of which three are not themable:
| Line | Color |
|---|---|
| a game | primaryColor |
| a folder | secondaryColor |
| a faded game | computed: primaryColor with the opacity halved |
| a faded folder | computed the same way |
| the background of a sort header | imposed |
Components present but inert
template_flag, template_genre and template_players are read by the engine but never used: the decorations' colors are not themable. Do not waste time on them.
The gameclip view — the screensaver
What shows when the machine is doing nothing.
The screensaver plays game clips. The theme dresses it up around them.
What is themable
This view is built almost entirely from your extras: place your components with extra="true" and they show.
A single reserved component is themable here: the video. The information components (md_rating, md_developer, md_genre…) exist in this view but are disabled theme-side: unlike detailed, you cannot format them.
➡️ To display the game's name or developer in the screensaver, place your own text with the variables:
<view name="gameclip">
<extras>
<text name="titre">
<pos>0.06 0.85</pos>
<text>${game.name}</text>
<fontSize>0.05</fontSize>
<color>FFFFFF</color>
</text>
<text name="editeur">
<pos>0.06 0.91</pos>
<text>${game.developer} · ${game.releasedate}</text>
<fontSize>0.03</fontSize>
</text>
</extras>
</view>
The game variables work here
This view's context contains the system AND the game: all the ${game.*} are resolved. That is what makes the screensaver interesting to dress up.
The menu view — the menus
What a theme can, and cannot, change about the menus.
Recalbox builds its menus itself: their content, their order and their labels are not yours.
What the theme provides are styles — and a single measurement.
<view name="menu">
<menuBackground>
<color>101820F0</color>
<path>${root}/data/arts/cadre.png</path>
</menuBackground>
<menuText>
<fontPath>${root}/data/fonts/Exo2.otf</fontPath>
<fontSize>0.038</fontSize>
<color>C6CBD8</color>
<selectedColor>101820</selectedColor>
<selectorColor>4FE3C1</selectorColor>
<separatorColor>FFFFFF20</separatorColor>
</menuText>
<menuSize><height>0.85</height></menuSize>
</view>
The nine available components are described in Menu styling.
What you cannot do
- add, remove or rename a menu line;
- change the order of the sections;
- place a free component in a menu: extras are not read in this view.
What to keep in mind
The menus display on top of the current view. The veil (fadePath of menuBackground) darkens what is behind: without it, a menu with a transparent background becomes unreadable on a light theme.
A layer present on every view
A shared backdrop written once, instead of being copied into each view.
The problem
A CRT veil, a brand logo, a frame: you want it on the system list and on the game list and on the menu. Copying it into each view works… until the day you edit only one of them. The others stay behind, and nothing tells you.
The solution
Right-click the layer (or the inspector's ⋯) → “Present on every view”.
The layer leaves its view and joins the shared layers: there is now only one, shared. Adjusting it from anywhere adjusts it everywhere.
To go back: “Keep only on this view”.
What it writes in the theme
A single block, whose name lists the views:
<!-- global.xml -->
<view name="system, basic, detailed, menu, gameclip">
<extras>
<image name="voileCRT" extra="true">…</image>
</extras>
</view>
The machine splits this name on the commas and places the element in each view. The file is loaded before the views: what applies everywhere is a base, which a view can still correct.
Not to be confused
“Duplicate to another layout…” is something else: it copies the layer into another layout of the same view (for example “Vertical left” and “Horizontal”), and the two copies are then independent.
⚠️ The view name
menuis written because the theme asks for it, but the engine reads no free component there: the layer does not show in the menus.
At import, a theme that already writes a block covering every view gets its shared layer back. A block that only covers some of them stays split per view: it only applies where the theme put it.
Using variables
Displaying a game's name, picking an image per system.
A variable is written ${…} and Recalbox replaces it at display time.
<text name="titre">
<text>Bienvenue sur ${system}</text>
</text>
→ “Bienvenue sur Super Nintendo”.
In an image path: the most useful
<image name="console" extra="true">
<path>${root}/data/arts/consoles/${system.name}.png</path>
</image>
One single line, and each system displays its own image. The files just have to bear the system's internal name: snes.png, megadrive.png…
Where each variable works
This is the rule that surprises the most, and it comes from the engine:
| Family | Views where it is resolved |
|---|---|
${system…} | Systems and Game list — a list always belongs to a system |
${game…} | Game list and Screensaver — wherever there is a hovered game |
${recalbox…} ${settings…} ${hardware…} ${display…} | everywhere |
A variable used where it does not exist is not replaced: the raw text shows as is, ${game.name} included. The studio only offers the ones that work on the current view.
When the highlighted row is not a game
In the game list, the cursor does not only travel across games: it also lands on folders and on sort headers, those intertitles Recalbox adds as soon as the list is sorted by anything other than alphabetical order. The ${game…} variables still answer, but they then describe a row that has no game:
| Highlighted row | ${game.name} | ${game.releasedate} | ${game.file.name} |
|---|---|---|---|
| a game | its name | its release date | the rom file |
| a folder | the folder name | UNKNOWN | the folder name |
| a sort header | nothing | UNKNOWN | nothing |
A component showing game data therefore has nothing left to say on those rows — and yet it stays on screen, on top of the folder name Recalbox writes at the same moment. Restrict it to game rows:
<text name="sortie" extra="true" showIf="game">
<text>Released: ${game.releasedate}</text>
</text>
The studio takes care of it: as soon as a component uses game data, its visibility switches to “a game”. The component’s Visibility tab lets you open it back up to folders and headers whenever that is what you want.
${root} — never to forget
${root} designates the root of the selected theme. Without it, paths are relative to the file that writes them, and your theme breaks as soon as it is stored differently.
<path>${root}/data/arts/fond.jpg</path> <!-- ✅ -->
<path>../data/arts/fond.jpg</path> <!-- fragile -->
The old $… variables
In old themes you will come across variables without braces:
| Old | What it gives | Current equivalent |
|---|---|---|
$system | the short name — “snes” | ${system.name} |
$theme | the theme's folder | ${root} |
⚠️ $system and ${system} do not give the same thing: the first yields “snes”, the second “Super Nintendo”. They are still accepted, but deprecated: only write the braced form from now on.
The random draw
<path>${random.between(fond1.jpg,fond2.jpg,fond3.jpg)}</path>
<fontSize>${random.range(1,10)}</fontSize>
random.between picks a value at random from the list, random.range a number between two bounds. The draw happens when the theme loads, not on every display.
All the variables
The complete list, and where each one works.
Here are all the variables Recalbox can replace, surveyed from the engine.
A variable is written ${…} and Recalbox replaces it at display time.
The system
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${system} | System name | The full name — e.g. “Sega Megadrive” | Systems, Games |
${system.input.keyboard} | Keyboard needed? | mandatory · recommended · optional · no | Systems, Games |
${system.input.mouse} | Mouse needed? | mandatory · recommended · optional · no | Systems, Games |
${system.input.pad} | Controller needed? | mandatory · recommended · optional · no | Systems, Games |
${system.logo} | System logo | The path of the logo provided by Recalbox | Systems, Games |
${system.manufacturer} | Manufacturer | E.g. “Sega”, “Nintendo”. Empty if unknown | Systems, Games |
${system.name} | Short system name | — | Systems, Games |
${system.releasedate} | Year of release | Year and month — e.g. “1988-10” | Systems, Games |
${system.type} | Machine type (technical name) | arcade · console · handheld · computer · engine · port · fantasy · virtual · virtual-arcade | Games, Systems |
${system.type.name} | Machine type | The same, in plain words: “Home Console”, “handheld Console”, “Arcade”… | Systems, Games |
The game
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${game.developer} | Developer | E.g. “Konami”. “UNKNOWN” if absent | Screensaver, Games |
${game.file.name} | File name | The file name, extension included | Screensaver, Games |
${game.file.path} | Full file path | The full path of the file | Screensaver, Games |
${game.file.stem} | File name (without extension) | The file name, without the extension | Screensaver, Games |
${game.genre.normalized} | Genre (technical name) | The normalized genre, in English — “Platform”, “Shoot’em Up”, “Racing”… | Screensaver, Games |
${game.genre.raw} | Genre | The genre as written in the game's sheet | Screensaver, Games |
${game.isadult} | Adults only? | yes or no | Screensaver, Games |
${game.isfavorite} | Is a favorite? | yes or no (never true/false) | Screensaver, Games |
${game.ishidden} | Is hidden? | yes or no | Screensaver, Games |
${game.islastversion} | Is the latest version? | yes or no | Screensaver, Games |
${game.isnotagame} | Is not a game? | yes or no | Screensaver, Games |
${game.ispreinstalled} | Is preinstalled? | yes or no | Games, Screensaver |
${game.license} | License | The license, often empty | Screensaver, Games |
${game.name} | Game name | The name of the game | Screensaver, Games |
${game.players} | Number of players | “1”, “2”, “1-4”, “4+”… | Screensaver, Games |
${game.players.max} | Players — maximum | A number — e.g. “4” | Screensaver, Games |
${game.players.min} | Players — minimum | A number — e.g. “1” | Screensaver, Games |
${game.publisher} | Publisher | E.g. “Sega”. “UNKNOWN” if absent | Screensaver, Games |
${game.releasedate} | Release date | ISO date — e.g. “1991-06-23”. “UNKNOWN” if absent | Screensaver, Games |
${game.synopsis} | Description | The presentation text, often long | Screensaver, Games |
Rating and statistics
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${game.lastplayed} | Last played | ISO date, or “NEVER” if never played | Screensaver, Games |
${game.rating.10} | Rating (out of 10) | An integer from 0 to 10 | Screensaver, Games |
${game.rating.100} | Rating (out of 100) | An integer from 0 to 100 | Screensaver, Games |
${game.rating.5} | Rating (out of 5) | An integer from 0 to 5 — not stars | Screensaver, Games |
${game.timesplayed} | Number of plays | A number of plays | Screensaver, Games |
${game.totalplayed} | Total play time | A duration — e.g. “3h 12m”. “NONE” if zero | Screensaver, Games |
The game's images and video
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${game.media.boxpath} | Box art | — | Screensaver, Games |
${game.media.imagepath} | Cover art / image | The path of the cover art. Empty if the game has none — see ifexists | Screensaver, Games |
${game.media.thumbpath} | Thumbnail | The path of the thumbnail | Screensaver, Games |
${game.media.videopath} | Video | The path of the video | Screensaver, Games |
The game's medium
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${game.support.index} | Medium index | The disc number — empty if there is only one | Screensaver, Games |
${game.support.number} | Medium number | The whole thing assembled — e.g. “2A/3” | Screensaver, Games |
${game.support.side} | Medium side | The side of the medium — A, B… | Screensaver, Games |
${game.support.total} | Number of media | The number of media. “UNKNOWN” if unknown | Screensaver, Games |
${game.support.type} | Medium type | Cartridge · CD/DVD · Harddisk · Files · Tape · Quick Disc · 3" Floppy · 3".5 Floppy · 5".25 Floppy · PCB · Unknown | Screensaver, Games |
The game's system
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${game.system} | Name of the game's system | The full name of the game's system | Screensaver, Games |
${game.system.input.keyboard} | Keyboard required by the game's system | mandatory · recommended · optional · no | Screensaver, Games |
${game.system.input.mouse} | Mouse required by the game's system | mandatory · recommended · optional · no | Screensaver, Games |
${game.system.input.pad} | Controller required by the game's system | mandatory · recommended · optional · no | Screensaver, Games |
${game.system.logo} | Logo of the game's system | The path of its logo | Screensaver, Games |
${game.system.manufacturer} | Manufacturer of the game's system | Its manufacturer | Screensaver, Games |
${game.system.name} | Short name of the game's system | Its internal name | Screensaver, Games |
${game.system.releasedate} | Year of the game's system | Its year of release | Screensaver, Games |
${game.system.type} | Type of the game's system (technical name) | Like ${system.type}: console · handheld · arcade… | Screensaver, Games |
${game.system.type.name} | Type of the game's system | The same, in plain words | Screensaver, Games |
Emulator
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${game.emulator.compatibility} | Compatibility | unknown · low · average · good · high · perfect | Screensaver, Games |
${game.emulator.extensions} | Supported extensions | The supported extensions — e.g. “.bin .gen .md” | Screensaver, Games |
${game.emulator.hasnetplay} | Supports online play? | yes or no | Screensaver, Games |
${game.emulator.hassoftpatching} | Accepts patches? | yes or no | Screensaver, Games |
${game.emulator.islibretro} | Is a Libretro core? | yes or no | Screensaver, Games |
${game.emulator.name} | Emulator name | E.g. “libretro picodrive” | Screensaver, Games |
${game.emulator.speed} | Speed | unknown · low · average · good · high · perfect | Screensaver, Games |
The machine and its settings
| What to write | What it is | What it returns | Where |
|---|---|---|---|
${display.overscan} | Overscan? | yes or no | Screensaver, Systems, Menu, Games |
${random.between(a,b,c)} | A random value among… | one of the given values | everywhere |
${random.range(1,10)} | A random number between… | an integer between the two bounds | everywhere |
${display.resolution} | Resolution | fhd (1080p and above) · hd (720p) · vga · qvga | Screensaver, Systems, Menu, Games |
${display.tate} | Vertical screen (TATE)? | yes or no | Screensaver, Systems, Menu, Games |
${display.tateleft} | Rotated to the left? | yes or no | Systems, Menu, Games, Screensaver |
${display.tateright} | Rotated to the right? | yes or no | Screensaver, Systems, Menu, Games |
${hardware.board} | Machine model | The model — “RPi 5”, “PC x64”, “RG351P/M”… | Screensaver, Systems, Menu, Games |
${hardware.crt} | CRT screen? | yes or no | Menu, Games, Screensaver, Systems |
${hardware.isanbernic} | Is it an Anbernic? | yes or no | Screensaver, Systems, Menu, Games |
${hardware.isodroid} | Is it an Odroid? | yes or no | Screensaver, Systems, Menu, Games |
${hardware.ispc} | Is it a PC? | yes or no | Screensaver, Systems, Menu, Games |
${hardware.ispi} | Is it a Raspberry Pi? | yes or no | Screensaver, Systems, Menu, Games |
${hardware.jamma} | Jamma cabinet? | yes or no | Systems, Menu, Games, Screensaver |
${recalbox.built} | Build date | The build date | Screensaver, Systems, Menu, Games |
${recalbox.version} | Recalbox version | E.g. “10.0” | Screensaver, Systems, Menu, Games |
${root} | Theme folder | The root of the selected theme — to put in front of all your paths | Menu, Games, Screensaver, Systems |
${settings.language} | Language | The language alone — e.g. “fr” | Screensaver, Systems, Menu, Games |
${settings.locale} | Language and country | Language and country — e.g. “fr_FR” | Screensaver, Systems, Menu, Games |
${settings.region} | Chosen region | eu · us · jp | Screensaver, Systems, Menu, Games |
${settings.timezone} | Time zone | E.g. “Europe/Paris” | Screensaver, Systems, Menu, Games |
Your custom variables
Your own named values — a color, a font — created from the studio.
The variables on the previous page are Recalbox's: ${system}, ${game.developer}… They are provided, you only use them.
These ones are yours. You give a name to a value — a color, a font, a path — you use that name everywhere, and the day you change the value, the whole theme follows. That is what makes color options possible: a choice redefines the variable, and nothing else.
Creating them in the studio
- open Variables in the toolbar: the panel opens on the right;
- + New variable, give it a name, then say what it is — a color (you get the color picker, transparency included), a font (chosen among the theme's), or another value (a text, a path, or another variable);
- in a color field, the little { } next to the field offers yours, and in the font list they appear at the top.
The studio displays the computed value under the field: a variable that uses another one can be read at a glance.
By hand, without the studio
They are written in a <variables> block. The tag, the naming rules, the scope and the conditions are explained on Your own variables, in “A theme's structure”.
Displaying conditionally
The 26 conditions: the machine, the screen, and the displayed system.
An if= condition makes a component appear only in certain cases.
<image name="fond" if="crt">…</image>
They combine with ! (not), & (and), | (or) — or, if you prefer words, not, and, or: if="crt and not tate". See Combining several conditions.
⚠️ An identifier missing from this list is ignored by Recalbox and reported as unknown in themes.log.
Everywhere — the machine and the screen
| Condition | True when… |
|---|---|
hd fhd vga qvga | the screen has that resolution |
crt | the screen is a CRT tube |
overscan | the picture overscans (CRT without Jamma) |
tate tateleft tateright | the screen is vertical |
jamma bartop | the machine is an arcade cabinet |
ispc ispi isodroid isanbernic | the machine is that model |
nomenu | menus are disabled |
Only where a system is displayed
These eight query the current system. They only apply in the Systems view and the Game list — elsewhere there is no system, and the condition is always false.
| Condition | True when… |
|---|---|
console handheld computer arcade engine port fantasy | the system is of that type |
virtual | it is an automatic system (Favorites, Last played, All games…) |
favorite | it is the automatic Favorites system |
lastplayed | it is the automatic Last played system |
⚠️
favoritedoes not mean the highlighted game is a favorite: it refers to the system. There is no condition on a game or a folder — to react to a game’s content, use its variables (${game.isfavorite}) orifexists.
Combining several conditions
AND, OR, “not that”, and groups in parentheses.
One tag per condition
You pick a condition from the list, it becomes a tag. The little button in front says which way it counts:
- if — only in that case;
- if not — everywhere except that case.
On a tag already placed, that same button flips the direction; the ✕ removes it.
Groups
A group is a parenthesis. Inside, you say whether all the conditions are needed, or at least one. Between two groups, the same question arises: AND or OR.
Three spellings for the same operators
| What you mean | Sign | Spelled out |
|---|---|---|
| and | & | AND |
| or | | | OR |
| not | ! | NOT |
The two spellings mix, and case does not matter at all: if="crt AND !tate" equals if="crt & !tate".
⚠️ && and || do not work. Two signs in a row are a syntax error, and an expression in error is worth false: the component disappears with nothing saying so on screen.
Example: “on a CRT screen, and in arcade or in favorites” is written
crt & (arcade | favorite)
⚠️ The order of the groups matters
The machine reads left to right, with no precedence. a | b & c is worth (a | b) & c there, not a | (b & c). That is why the studio systematically adds the parentheses: what you read is exactly what the machine will understand.
Inside one group, on the other hand, the order does not matter at all: “not Full HD and arcade” says the same thing as “arcade and not Full HD”.
A condition is written in two places
The same condition, on the component itself or on a single one of its properties, does not say the same thing:
<image name="filtre" if="crt">…</image> <!-- ① the component ONLY exists on a CRT -->
<text name="titre"> <!-- ② the component always exists… -->
<fontSize if="crt">0.09</fontSize> <!-- …but its size changes on a CRT -->
<fontSize if="!crt">0.05</fontSize>
</text>
① on the component: it appears, or it does not exist at all. ② on a property: the component is always there, only one of its values changes.
In the studio, it is the same button and the same window in both cases — what you build here serves there identically.
The systems
The internal names expected in your folders and your files.
Each system has several names, and they must not be confused:
- The system — its usual name, the one you talk about. It never appears in any file;
- the variables, filled in by the engine at display time.
The folder to create in your theme is named after ${system.name}. A few systems are exceptions: they are listed below their table.
Machines (121)
| System | ${system.name} | ${system} | ${system.manufacturer} | ${system.type} |
|---|---|---|---|---|
| 240ptestsuite | 240ptestsuite | 240ptestsuite | virtual | virtual |
| Acorn BBC Micro | bbcmicro | Acorn BBC Micro | Acorn | computer |
| Amiga AGA | amiga1200 | Amiga AGA | Commodore | computer |
| Amiga CD32 | amigacd32 | Amiga CD32 | Commodore | console |
| Amiga CDTV | amigacdtv | Amiga CDTV | Commodore | console |
| Amiga ECS/OCS | amiga600 | Amiga ECS/OCS | Commodore | computer |
| Amstrad GX4000 | gx4000 | Amstrad GX4000 | Amstrad | console |
| AmstradCPC | amstradcpc | AmstradCPC | Amstrad | computer |
| Apple II | apple2 | Apple II | Apple | computer |
| Apple IIGS | apple2gs | Apple IIGS | Apple | computer |
| Apple Macintosh | macintosh | Apple Macintosh | Apple | computer |
| Arduboy | arduboy | Arduboy | fantasy | fantasy |
| Atari 2600 | atari2600 | Atari 2600 | Atari | console |
| Atari 5200 | atari5200 | Atari 5200 | Atari | console |
| Atari 7800 | atari7800 | Atari 7800 | Atari | console |
| Atari 8bits | atari800 | Atari 8bits | Atari | computer |
| Atari Jaguar | jaguar | Atari Jaguar | Atari | console |
| Atari ST | atarist | Atari ST | Atari | computer |
| Colecovision | colecovision | Colecovision | Coleco | console |
| Commodore 64 | c64 | Commodore 64 | Commodore | computer |
| Commodore VIC-20 | vic20 | Commodore VIC-20 | Commodore | computer |
| Daphne | daphne | Daphne | Daphne | arcade |
| DICE | dice | DICE | DICE | arcade |
| Dos (x86) | dos | Dos (x86) | IBM | computer |
| Dragon 32/64 | dragon | Dragon 32/64 | DragonData | computer |
| EasyRPG | easyrpg | EasyRPG | virtual | engine |
| Elektronika BK | bk | Elektronika BK | Elektronika | computer |
| Epoch Cassette Vision | cassettevision | Epoch Cassette Vision | Epoch | console |
| Exelvision EXL 100 | exl100 | Exelvision EXL 100 | Exelvision | computer |
| Fairchild Channel F | channelf | Fairchild Channel F | Fairchild | console |
| Family Computer Disk System | fds | Family Computer Disk System | Nintendo | console |
| FinalBurn Neo | fbneo | FinalBurn Neo | FBN | arcade |
| Game and Watch | gw | Game and Watch | Nintendo | handheld |
| Game Boy | gb | Game Boy | Nintendo | handheld |
| Game Boy Advance | gba | Game Boy Advance | Nintendo | handheld |
| Game Boy Color | gbc | Game Boy Color | Nintendo | handheld |
| GameCube | gamecube | GameCube | Nintendo | console |
| Infocom Z-Machine | zmachine | Infocom Z-Machine | Infocom | engine |
| LowRes NX | lowresnx | LowRes NX | virtual | fantasy |
| Lutro | lutro | Lutro | virtual | fantasy |
| Lynx | lynx | Lynx | Atari | handheld |
| Mame | mame | Mame | Mame | arcade |
| Mattel Intellivision | intellivision | Mattel Intellivision | Mattel | console |
| MegaDuck | megaduck | MegaDuck | Welback | handheld |
| MGT SAM Coupé | samcoupe | MGT SAM Coupé | MGT | computer |
| Moonlight | moonlight | Moonlight | NVidia | virtual |
| MSX1 | msx1 | MSX1 | Microsoft | computer |
| MSX2 | msx2 | MSX2 | Microsoft | computer |
| MSXturboR | msxturbor | MSXturboR | Microsoft | computer |
| NEC PC-88 | pc88 | NEC PC-88 | NEC | computer |
| NEC PC-98 | pc98 | NEC PC-98 | NEC | computer |
| NEC PC-FX | pcfx | NEC PC-FX | NEC | console |
| Neo-Geo AES | neogeo | Neo-Geo AES | SNK | console |
| Neo-Geo CD | neogeocd | Neo-Geo CD | SNK | console |
| Neo-Geo Pocket | ngp | Neo-Geo Pocket | SNK | handheld |
| Neo-Geo Pocket Color | ngpc | Neo-Geo Pocket Color | SNK | handheld |
| Nintendo 64 | n64 | Nintendo 64 | Nintendo | console |
| Nintendo 64DD | 64dd | Nintendo 64DD | Nintendo | console |
| Nintendo DS | nds | Nintendo DS | Nintendo | handheld |
| Nintendo Entertainment System | nes | Nintendo Entertainment System | Nintendo | console |
| Odyssey2 | o2em | Odyssey2 | Magnavox | console |
| OpenBOR | openbor | OpenBOR | Senile Team | engine |
| Oric/Atmos | oricatmos | Oric/Atmos | Tangerine | computer |
| Othello Multivision | multivision | Othello Multivision | Tsukuda | console |
| Palm | palm | Palm | Palm | handheld |
| Panasonic 3DO | 3do | Panasonic 3DO | Panasonic | console |
| PC Engine | pcengine | PC Engine | NEC | console |
| PC Engine CD | pcenginecd | PC Engine CD | NEC | console |
| Philips CD-I | cdi | Philips CD-I | Phillips | console |
| Philips P2000T | p2000t | Philips P2000T | Philips | computer |
| Philips VG 5000 | vg5000 | Philips VG 5000 | Philips | computer |
| PICO-8 | pico8 | PICO-8 | virtual | fantasy |
| Pocket Challenge v2 | pcv2 | Pocket Challenge v2 | Benesse | handheld |
| Pokémon Mini | pokemini | Pokémon Mini | Nintendo | handheld |
| Sammy Atomiswave | atomiswave | Sammy Atomiswave | Sammy | arcade |
| Satellaview | satellaview | Satellaview | Nintendo | console |
| Screenshots | imageviewer | Screenshots | virtual | virtual |
| ScummVM | scummvm | ScummVM | Ludvig Strigeus | engine |
| Sega 32X | sega32x | Sega 32X | Sega | console |
| Sega CD | segacd | Sega CD | Sega | console |
| Sega Dreamcast | dreamcast | Sega Dreamcast | Sega | console |
| Sega Game Gear | gamegear | Sega Game Gear | Sega | handheld |
| Sega Master System / Mark III | mastersystem | Sega Master System / Mark III | Sega | console |
| Sega Megadrive | megadrive | Sega Megadrive | Sega | console |
| Sega Model3 | model3 | Sega Model3 | Sega | arcade |
| Sega NAOMI | naomi | Sega NAOMI | Sega | arcade |
| Sega NAOMI 2 | naomi2 | Sega NAOMI 2 | Sega | arcade |
| Sega NAOMI GD-ROM System | naomigd | Sega NAOMI GD-ROM System | Sega | arcade |
| Sega Pico | pico | Sega Pico | Sega | console |
| Sega Saturn | saturn | Sega Saturn | Sega | console |
| Sega SG1000 | sg1000 | Sega SG1000 | Sega | console |
| Sharp X1 | x1 | Sharp X1 | Sharp | computer |
| Sharp X68000 | x68000 | Sharp X68000 | Sharp | computer |
| Solarus | solarus | Solarus | Solarus | engine |
| Sony Playstation 1 | psx | Sony Playstation 1 | Sony | console |
| Sony Playstation 2 | ps2 | Sony Playstation 2 | Sony | console |
| Sony Playstation Portable | psp | Sony Playstation Portable | Sony | handheld |
| Spectravideo | spectravideo | Spectravideo | Spectravideo | computer |
| ST-V | stv | ST-V | Sega | arcade |
| SuFami Turbo | sufami | SuFami Turbo | Bandai | console |
| Super Cassette Vision | scv | Super Cassette Vision | Epoch | console |
| Super Nintendo Entertainment System | snes | Super Nintendo Entertainment System | Nintendo | console |
| Supergrafx | supergrafx | Supergrafx | NEC | console |
| Texas Instrument TI-99/4A | ti994a | Texas Instrument TI-99/4A | Texas Instrument | computer |
| Thomson | thomson | Thomson | Thomson | computer |
| TIC-80 | tic80 | TIC-80 | port | fantasy |
| TRS-80 Color Computer | trs80coco | TRS-80 Color Computer | Tandy | computer |
| Uzebox | uzebox | Uzebox | port | console |
| Vectrex | vectrex | Vectrex | MB | console |
| Videopac+ G7400 | videopacplus | Videopac+ G7400 | Philips | console |
| Vircon32 | vircon32 | Vircon32 | virtual | console |
| Virtual Boy | virtualboy | Virtual Boy | Nintendo | console |
| Visual Pinball Standalone | vpinball | Visual Pinball Standalone | Randy Davis | engine |
| WASM-4 | wasm4 | WASM-4 | Bruno Garcia | fantasy |
| Watara Supervision | supervision | Watara Supervision | Watara | handheld |
| Wii | wii | Wii | Nintendo | console |
| WonderSwan | wswan | WonderSwan | Bandai | handheld |
| WonderSwan Color | wswanc | WonderSwan Color | Bandai | handheld |
| Xbox | xbox | Xbox | Microsoft | console |
| ZX81 | zx81 | ZX81 | Sinclair | computer |
| ZXSpectrum | zxspectrum | ZXSpectrum | Sinclair | computer |
The folders that do not carry the system name. For these, and only these, the folder in your theme is named something other than ${system.name}.
| System | ${system.name} | Folder to create |
|---|---|---|
| Dos (x86) | dos | pc |
| GameCube | gamecube | gc |
| Odyssey2 | o2em | odyssey2 |
| Oric/Atmos | oricatmos | oric |
| Thomson | thomson | to8 |
| WonderSwan | wswan | wonderswan |
| WonderSwan Color | wswanc | wonderswancolor |
The virtual systems (11)
Recalbox builds them itself, from your games: they have no system file, but they do have a theme folder, and you can dress them up like any other.
| System | ${system.name} | ${system} | ${system.type} |
|---|---|---|---|
| Ports | ports | Ports | virtual |
| Favorites | favorites | Favorites | virtual |
| Last played | lastplayed | Last played | virtual |
| All games | allgames | All games | virtual |
| Multiplayer | multiplayer | Multiplayer | virtual |
| Arcade | arcade | Arcade | virtual-arcade |
| Lightgun | lightgun | Lightgun | virtual |
| Tate | tate | Tate | virtual |
| Dial | dial | Dial | virtual |
| Trackball | trackball | Trackball | virtual |
| Challenges | challenges | Challenges | virtual |
The folders that do not carry the system name. For these, and only these, the folder in your theme is named something other than ${system.name}.
| System | ${system.name} | Folder to create |
|---|---|---|
| Last played | lastplayed | auto-lastplayed |
| All games | allgames | auto-allgames |
| Multiplayer | multiplayer | auto-multiplayer |
| Lightgun | lightgun | auto-lightgun |
| Tate | tate | auto-tate |
| Dial | dial | auto-dial |
| Trackball | trackball | auto-trackball |
| Challenges | challenges | auto-challenges |
Arcade by manufacturer (54) (${system.type} = virtual-arcade)
| System | ${system.name} | ${system} |
|---|---|---|
| Acclaim | arcade-manufacturer-acclaim | Acclaim |
| Atari | arcade-manufacturer-atari | Atari |
| Atlus | arcade-manufacturer-atlus | Atlus |
| Banpresto | arcade-manufacturer-banpresto | Banpresto |
| Capcom Cps1 | arcade-manufacturer-capcom-cps1 | Capcom Cps1 |
| Capcom Cps2 | arcade-manufacturer-capcom-cps2 | Capcom Cps2 |
| Capcom Cps3 | arcade-manufacturer-capcom-cps3 | Capcom Cps3 |
| Capcom | arcade-manufacturer-capcom | Capcom |
| Cave | arcade-manufacturer-cave | Cave |
| Data east | arcade-manufacturer-data east | Data east |
| Exidy | arcade-manufacturer-exidy | Exidy |
| Hng64 | arcade-manufacturer-hng64 | Hng64 |
| Igs | arcade-manufacturer-igs | Igs |
| Irem M72 | arcade-manufacturer-irem-m72 | Irem M72 |
| Irem M92 | arcade-manufacturer-irem-m92 | Irem M92 |
| Irem | arcade-manufacturer-irem | Irem |
| Itech | arcade-manufacturer-itech | Itech |
| Jaleco | arcade-manufacturer-jaleco | Jaleco |
| Kaneko | arcade-manufacturer-kaneko | Kaneko |
| Konami Gx | arcade-manufacturer-konami-gx | Konami Gx |
| Konami | arcade-manufacturer-konami | Konami |
| Midway | arcade-manufacturer-midway | Midway |
| Mitchell | arcade-manufacturer-mitchell | Mitchell |
| Namco Na | arcade-manufacturer-namco-na | Namco Na |
| Namco Nb | arcade-manufacturer-namco-nb | Namco Nb |
| Namco System1 | arcade-manufacturer-namco-system1 | Namco System1 |
| Namco System10 | arcade-manufacturer-namco-system10 | Namco System10 |
| Namco System11 | arcade-manufacturer-namco-system11 | Namco System11 |
| Namco System12 | arcade-manufacturer-namco-system12 | Namco System12 |
| Namco System18 | arcade-manufacturer-namco-system18 | Namco System18 |
| Namco System2 | arcade-manufacturer-namco-system2 | Namco System2 |
| Namco | arcade-manufacturer-namco | Namco |
| Neogeo | arcade-manufacturer-neogeo | Neogeo |
| Nichibutsu | arcade-manufacturer-nichibutsu | Nichibutsu |
| Nintendo | arcade-manufacturer-nintendo | Nintendo |
| Nmk | arcade-manufacturer-nmk | Nmk |
| Psikyo | arcade-manufacturer-psikyo | Psikyo |
| Raizing | arcade-manufacturer-raizing | Raizing |
| Sammy | arcade-manufacturer-sammy | Sammy |
| Sega Stv | arcade-manufacturer-sega-stv | Sega Stv |
| Sega System16 | arcade-manufacturer-sega-system16 | Sega System16 |
| Sega System18 | arcade-manufacturer-sega-system18 | Sega System18 |
| Sega System32 | arcade-manufacturer-sega-system32 | Sega System32 |
| Sega | arcade-manufacturer-sega | Sega |
| Seibu | arcade-manufacturer-seibu | Seibu |
| Seta | arcade-manufacturer-seta | Seta |
| Snk | arcade-manufacturer-snk | Snk |
| Taito F3 | arcade-manufacturer-taito-f3 | Taito F3 |
| Taito Gnet | arcade-manufacturer-taito-gnet | Taito Gnet |
| Taito | arcade-manufacturer-taito | Taito |
| Technos | arcade-manufacturer-technos | Technos |
| Tecmo | arcade-manufacturer-tecmo | Tecmo |
| Toaplan | arcade-manufacturer-toaplan | Toaplan |
| Visco | arcade-manufacturer-visco | Visco |
By genre (56) (${system.type} = virtual)
| System | ${system.name} | ${system} |
|---|---|---|
| Action | genre-action | Action |
| Platform | genre-actionplatformer | Platform |
| Platform Shooter | genre-actionplatformshooter | Platform Shooter |
| First Person Shooter | genre-actionfirstpersonshooter | First Person Shooter |
| Shoot'em Up | genre-actionshootemup | Shoot'em Up |
| Shoot with Gun | genre-actionshootwithgun | Shoot with Gun |
| Fighting | genre-actionfighting | Fighting |
| Beat'em All | genre-actionbeatemup | Beat'em All |
| Infiltration | genre-actionstealth | Infiltration |
| Battle Royale | genre-actionbattleroyale | Battle Royale |
| Rythm & Music | genre-actionrythm | Rythm & Music |
| Adventure | genre-adventure | Adventure |
| Textual Adventure | genre-adventuretext | Textual Adventure |
| Graphical Adventure | genre-adventuregraphics | Graphical Adventure |
| Visual Novel | genre-adventurevisualnovels | Visual Novel |
| Interactive Movie | genre-adventureinteractivemovie | Interactive Movie |
| Real Time 3D Adventure | genre-adventurerealtime3d | Real Time 3D Adventure |
| Survival | genre-adventuresurvivalhorror | Survival |
| RPG | genre-rpg | RPG |
| Action RPG | genre-rpgaction | Action RPG |
| MMORPG | genre-rpgmmo | MMORPG |
| Dungeon Crawler | genre-rpgdungeoncrawler | Dungeon Crawler |
| Tactical RPG | genre-rpgtactical | Tactical RPG |
| JRPG | genre-rpgjapanese | JRPG |
| Party based RPG | genre-rpgfirstpersonpartybased | Party based RPG |
| Simulation | genre-simulation | Simulation |
| Build & Management | genre-simulationbuildandmanagement | Build & Management |
| Life Simulation | genre-simulationlife | Life Simulation |
| Fishing & Hunting | genre-simulationfishandhunt | Fishing & Hunting |
| Vehicle Simulation | genre-simulationvehicle | Vehicle Simulation |
| Science Fiction Simulation | genre-simulationscifi | Science Fiction Simulation |
| Strategy | genre-strategy | Strategy |
| eXplore, eXpand, eXploit & eXterminate | genre-strategy4x | eXplore, eXpand, eXploit & eXterminate |
| Artillery | genre-strategyartillery | Artillery |
| Auto-battler | genre-strategyautobattler | Auto-battler |
| Multiplayer Online Battle Arena | genre-strategymoba | Multiplayer Online Battle Arena |
| Real Time Strategy | genre-strategyrts | Real Time Strategy |
| Turn Based Strategy | genre-strategytbs | Turn Based Strategy |
| Tower Defense | genre-strategytowerdefense | Tower Defense |
| Wargame | genre-strategywargame | Wargame |
| Sports | genre-sports | Sports |
| Racing | genre-sportracing | Racing |
| Sport Simulation | genre-sportsimulation | Sport Simulation |
| Competition Sport | genre-sportcompetitive | Competition Sport |
| Fighting/Violent Sport | genre-sportfight | Fighting/Violent Sport |
| Pinball | genre-pinball | Pinball |
| Board game | genre-board | Board game |
| Casual game | genre-casual | Casual game |
| Digital Cards | genre-digitalcard | Digital Cards |
| Puzzle & Logic | genre-puzzleandlogic | Puzzle & Logic |
| Multiplayer Party Game | genre-party | Multiplayer Party Game |
| Trivia | genre-trivia | Trivia |
| Casino | genre-casino | Casino |
| Multi Game Compilation | genre-compilation | Multi Game Compilation |
| Demo from Demo Screne | genre-demoscene | Demo from Demo Screne |
| Educative | genre-educative | Educative |
The fonts
The machine's, and yours.
A font comes from two places, and that changes what must be shipped.
The machine's
<fontPath>:/ubuntu_condensed.ttf</fontPath>
The :/ designates EmulationStation's resources. Nothing to copy into your theme: the file is present on every Recalbox.
Yours
<fontPath>${root}/data/fonts/Exo2.otf</fontPath>
The file must be in your theme, and will ship with it. TTF and OTF work.
The size
fontSize changes unit depending on its value: below 1, it is a proportion of the screen height; from 1 up, it is pixels. Everything is explained in Ratio, percentage or pixels. Prefer the ratio: 0.045 gives the same proportion everywhere.
Pixel fonts
f8bitfortressplus is only sharp at sizes that are multiples of 7 in pixels. Between those values, it smears. That is true of every font drawn pixel by pixel: if you add one, check at which sizes it is clean.
fontStyle
normal · bold · italic · bolditalic
⚠️ Only works if the font contains those weights. A font shipped as a single “Regular” file will not become bold: you must provide the “Bold” file and point to it with its own fontPath.
Shipped with Recalbox
Nothing to copy into your theme.
| Font | What to write |
|---|---|
| Ubuntu Condensed | :/ubuntu_condensed.ttf |
| DejaVu Sans Condensed | :/dejavusanscondensed.ttf |
| Ubuntu Mono | :/UbuntuMonoR.ttf |
| 8-bit Fortress Plus — pixel font, crisp at multiples of 7 | :/f8bitfortressplus.ttf |
What an option is
Offering choices to your theme's user.
An option is a property the user will find in their machine's menus: “Theme colors: Blue / Green / Red”, “Retro filter: CRT / scanlines / none”.
It is what distinguishes a rich theme from a frozen one.
What an option really does
An option is not a “show or hide” switch. It is a file loaded on top of the theme, which redefines whatever it wants: a color, a font, an entire layout.
On the official themes, the vast majority of options only change colors — often by redefining a simple <variable>. Hidings are rare.
An option acts on every view at once: it is a property of the theme, not of a view.
The official theme's options
recalbox-next-2025 offers ten of them, which gives a good idea of what is done:
| Option | What it changes |
|---|---|
systemView | the layout of the system list |
gameList | that of the game list |
gameclipview | that of the screensaver |
SysInfos | the systems' information: full, minimal, hidden |
gameInfos | the games' information |
colorTheme | 12 palettes of colors |
shader | the retro filter: CRT, scanlines, honeycomb, none |
shadow | the shading |
bands | the color bands: thin, thick, none |
iconesetTheme | the help bar's icons: 8 sets |
Declaring an option: <subset>
The exact syntax, in two steps.
An option is built in two steps: you declare it, then you list its choices.
1. Declare the group
<subset subset="colorTheme"
title="THEME : Colors" title.fr="THÈME : Couleurs"
help="Choose the color set" help.fr="Choisissez la palette" />
| Attribute | Role |
|---|---|
subset | the identifier of the group — it is what ties the choices together |
title | the label the user reads in the menu |
help | the explanation line under the label |
title and help accept a language suffix: title.fr, title.es… The version without a suffix serves as the fallback.
2. List the choices
Each choice is an <include> bearing the same subset:
<include subset="colorTheme" name="Blue" name.fr="Bleu">${root}/options/couleurs/bleu.xml</include>
<include subset="colorTheme" name="Green" name.fr="Vert">${root}/options/couleurs/vert.xml</include>
<include subset="colorTheme" name="Red" name.fr="Rouge">${root}/options/couleurs/rouge.xml</include>
| Attribute | Role |
|---|---|
subset | which group this choice belongs to |
name | the choice's label in the list (translatable: name.fr) |
The “none” choice
An empty <include> gives the “none” option — useful to leave the theme in its original state:
<include subset="shader" name="None" name.fr="Aucun"></include>
Offering a choice only on certain screens
<include subset="systemView" if="(hd | fhd) and !tate"
name="Vertical left" name.fr="Vertical gauche">${root}/_views/vertical.xml</include>
<include subset="systemView" if="crt | jamma"
name="Horizontal">${root}/_views/horizontal.xml</include>
The user only sees the choices relevant to their hardware. That is how the retro filter does not appear on a CRT screen, which does not need it.
The content of a choice file
It is an ordinary theme file, which only redefines what changes:
<?xml version="1.0" encoding="UTF-8"?>
<theme>
<variables>
<variable name="CouleurPrincipale" value="7C2E44" />
</variables>
</theme>
Three useful lines, and the whole theme turns red — provided the theme was built on variables rather than on hard-coded colors.
⚠️ Options load BEFORE the views. A variable applies to what is read after it: that is where the choice must pass for the views to benefit from it.
The order to write in theme.xml:
<theme name="Mon Thème" …>
<include>${root}/variables.xml</include> <!-- 1. the default values -->
<include>${root}/options.xml</include> <!-- 2. the choice redefines them -->
<include>${root}/views/system.xml</include> <!-- 3. the views, which use them -->
</theme>
What happens when the person changes their choice
Recalbox re-reads the whole theme — every file, starting from theme.xml, with the new choice active. That is why a brief “Updating theme…” shows at that moment.
An option that only changes colors redefines variables only. It is the shortest way to write a color set — and the main reason to use
<variables>.
The order of the choices
The numeric prefix — and why it does not show.
Recalbox does not keep the order in which you write your <include>. It sorts the choices itself, in two steps:
- if ALL the choices have a number at the head of their
name, it sorts by that number; - otherwise, it sorts by the
name's alphabetical order.
That is why you number them.
The right way to write it
<include subset="bands" name="1 - Thin" name.fr="1 - Fines">…</include>
<include subset="bands" name="2 - Thick" name.fr="2 - Épaisses">…</include>
<include subset="bands" name="3 - None" name.fr="3 - Aucune">…</include>
The prefix does not show. Recalbox spots it, uses it to sort, then removes it before displaying the label. The user reads “Thin”, “Thick”, “None”.
What the engine accepts as a prefix
A number, then a space, a dash or a period, all within the first eight characters. These three spellings work:
1 - Thin
1. Thin
1 Thin
The trap
Numeric sorting is only used if all the choices are numbered. One single omission, and Recalbox falls back to alphabetical order — your choices reorder themselves, with no message.
<include subset="bands" name="1 - Fines">…</include>
<include subset="bands" name="2 - Épaisses">…</include>
<include subset="bands" name="Aucune">…</include> <!-- ❌ breaks the sort of all three -->
The FILE names, however, are free
Not to be confused: the number goes in the name attribute, not in the file name. Recalbox never looks at what your file is called.
options/
bandes/
fines.xml ← name them however you want
epaisses.xml
aucune.xml
What matters: one folder per option, and names that say what they do.
Trying out your theme
Copying it onto the machine, and reading the log when something goes wrong.
A theme can only be judged switched on, on a screen. The studio's preview is faithful, but nothing replaces the machine.
- Export the theme from the studio;
- copy the folder into
/recalbox/share/themes/; - on the machine: Menu → Interface properties → Theme, and pick yours.
📄 The log: themes.log
This is the first place to look when something does not show up.
/recalbox/share/system/logs/themes.log
It records, with file and line to back it up:
- an unknown property — often a case mistake (
keepRatioinstead ofkeepratio); - a component whose type does not exist;
- an
Extra type unknown: …— you placed as anextraa type that does not accept it; - a variable without
nameor withoutvalue; - a badly written pair (
pos,size).
An invisible component with nothing in the log almost always means extra="true" is missing: see The most important rule.
Sharing your theme
Submit your theme to the Recalbox theme manager, straight from the studio.
Recalbox has a theme manager: themes install from there, with nothing to copy by hand. To get a theme in, you submit it — and everything happens from this studio.
📘 No repository to fork, no merge request, no file to write: the studio builds the package, takes the screenshots and submits everything for you.
Submitting your theme
Two doors, one destination:
- Your theme is in the studio — the “Export or publish” button, “Publish” tab;
- Your theme is a folder or a zip — “Import an existing theme”, then “submit it for publication”.
Either way, you describe your theme in a few fields and send it. That's all.
⚠️ You need the right to do it. Submitting a theme is open to the roles the team has picked on the Recalbox Discord: if the button isn't there, you don't have it yet.
What the studio does for you
- it photographs your theme — every view, every resolution you announce, and one image per theme option — then keeps the best ones;
- it writes the theme record (name, version, author, description, supported displays);
- it uploads the files and lets everyone know.
⚠️ Your archive is sent as is. The studio only looks inside it to take the screenshots; it never rewrites it. Your folders, your file names, your layout: nothing moves.
What happens next
- your theme goes to the vote — theme makers look at it screen by screen and give their opinion, with a note if they want to;
- an administrator decides. The vote informs, it does not decide: no one is published by a tally;
- accepted, the theme enters the theme manager and every Recalbox sees it. Refused, you get a reason — enough to fix it and submit again.
You follow all of it from “My themes”: your theme's card shows “request pending”, and the “Publish” tab shows the tally and what people wrote (without their names).
Updating a published theme
Same road: submit your theme again, with a higher version number.
⚠️ An update does not go through the vote — it's your theme, you know its state. It only waits for an administrator to put it online. Nobody has to reinstall anything: the theme manager offers them the update.
What gets you refused
- an incomplete theme: an empty view, a display announced but never worked on;
- images that aren't yours;
- compatibility announced without trying it — claiming CRT when you have never looked at it on a tube shows immediately.