Making a Recalbox theme

45 pages · 8 sections
FRENESDE Show everything Page by page Create my theme

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

What a theme does not decide

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

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

  1. Start from a template. A blank page is the worst starting point. A template comes with its components already in place: you replace them.
  2. 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.
  3. Place your components: drag them from the left column, adjust them in the right one.
  4. 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.
  5. Export, copy the folder to your machine, try it — see Trying your theme.

By hand

  1. 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.
  2. Create the folder and its theme.xml — the only mandatory file. See Folders and files.
  3. Announce what you target in the <theme> tag: compatibility for the screen types, resolutions for 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.
  4. 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.
  5. Serve the other screens with conditions rather than copying everything: <include if="crt">.
  6. Copy the folder to your machine and try it — and read themes.log at 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/.

📁 mon-theme/ 📄 theme.xml 📁 views/ 📁 data/ 📁 data/fonts/ …arranged however you like REQUIRED without it, the theme does not exist free no name, no imposed layout the only rule: a folder, and a theme.xml at its root

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 Englishviews/, 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

AttributeRoleExampleIf missing
nameName shown in the theme listname="My Theme"the folder name
versionTheme versionversion="1.2"not shown
authorThe authorauthor="Benoît"not shown
recalboxMinimum Recalbox version requiredrecalbox="10.0"all versions
compatibilitySupported screen types: hdmi, crt, jamma, tatecompatibility="hdmi,crt"hdmi
resolutionsSupported resolutions: qvga, vga, hd, fhdresolutions="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.xmlthe 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.

base.xml background = blue rouge.xml background = red the background is RED read top to bottom: two components with the same name replace each other, the LAST one wins that is the whole overlay mechanism — and therefore the options

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:

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

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

  <variable name="CheminLogo" value="${root}/data/logos/${system.name}.svg" />
  <variables if="crt">
    <variable name="TailleTitre" value="0.09" />
  </variables>

⚠️ 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 filevariables.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.

SpellingExampleWhat it means
Ratio (the default)0.5 0.25a proportion of the screen, from 0 to 1
Percentage50% 25%the same thing, written differently
Pixels960p 270preal 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:

ValueWhat it means
< 1a proportion of the screen's short side0.05 = 5% of the height in 16:9
>= 1a 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:

poswhere, on the SCREEN, the component is placed. originwhich 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.

the screen, whatever its resolution 00 10 01 11 pos 0.1 0.2 0.1 → 10 % de la largeur 0.2 → 20 % de la hauteur everything is measured from the top-left corner of the SCREEN

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:

your component000.501000.50.50.510.5010.5111the nine possible values of origin

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 screen (16:9) your image (landscape) origin 1 1 the bottom-right corner of the image… pos 0.5 0.5 …placed at the center of the screen ➜ the image is NOT centered: it sits above and left of 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

originThe component's point placed on pos
0 0top-left corner — the default value
0.5 0middle of the top edge
1 0top-right corner
0 0.5middle of the left edge
0.5 0.5the center
1 0.5middle of the right edge
0 1bottom-left corner
0.5 1middle of the bottom edge
1 1bottom-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 wantposorigin
centered on screen0.5 0.50.5 0.5
stuck to the right edge1 …1 …
stuck to the bottom edge… 1… 1
centered at the bottom0.5 10.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:

SpellingWhat is guaranteed
size 0.3 0the width is exactly 30%; the height follows, whatever it is — it may overflow
maxSize 0.3 0.2the 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>
size 0.3 0.2 the image FILLS the box… stretched image ➜ distorted maxSize 0.3 0.2 …the image FITS inside the box whole image ➜ proportions kept empty space is left above and below the yellow dotted frame = the requested box, in both cases

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, maxSize is almost always what you want. Logos do not have the same shape from one machine to the next: with size, 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.

rotationOrigin 0.5 0.5 it pivots on its center rotationOrigin 0 0 it pivots on its top-left corner rotation 20 — in degrees, clockwise

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.

help bar50texts and logos40readability veil20backdrop10background1zIndex

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 -->

The most useful transparency values

Everyone knows a color's code. The two opacity characters, much less:

OpacityTo writeOpacityTo write
0% — invisible0060%99
10%1A70%B3
20%3375%BF
25%4080%CC
30%4D90%E6
40%6695%F2
50%80100% — opaqueFF

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.

colorTop colorBottom vertical gradient colorLeft colorRight horizontal gradient colorTopLeft colorBottomRight four-corner gradient giving TWO colors is enough to get a gradient
PropertyEffect
colorTop + colorBottomvertical gradient
colorLeft + colorRighthorizontal gradient
colorTopLeft, colorTopRight, colorBottomLeft, colorBottomRightfour-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

SuffixWhat it targets
.fr .es .dethe machine's language, in lowercase
.fr_FRthe language and the country
.US .EU .JPthe 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">

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

TypeWhat it does
texta text, on one or several lines
scrolltexta text that scrolls when too long
markdowna formatted text (bold, headings, lists)
imagean image
videoa video
boxa color block, or a gradient
datetimea date
ratinga rating, as stars
sounda sound (nothing displayed)

Lists and navigation

TypeWhat it does
textlistthe game list
carouselthe system carousel
helpsystemthe 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>
PropertyTypeRole
pos size origin rotation rotationOriginpairplacement — see Position, size, origin
texttextthe content, variables included
pathpathreads the content from a file, instead of text
fontPathpaththe font
fontSizenumber< 1 = screen-height ratio, >= 1 = pixels
fontStyletextnormal, bold, italic, bolditalic
colorcolorthe text color
backgroundColorcolora background behind the text
alignmenttextsee below
forceUppercaseyes/noall capitals
lineSpacingnumberline spacing, 1.2 by default
multilineyes/noallow line breaks
zIndexnumberdepth
disabledyes/noswitch 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:

PositionTo writeSynonym
top-lefttopleft
top-centertopcentertop
top-righttopright
center-leftcenterleftleft
centercenter
center-rightcenterrightright
bottom-leftbottomleft
bottom-centerbottomcenterbottom
bottom-rightbottomright

⚠️ An unknown value does not keep the previous alignment: it falls back to center-left, the default value.

toplefttopcenter= toptoprightcenterleft= leftcentercenterright= rightbottomleftbottomcenter= bottombottomrightthe text sits inside its `size` box — without size, alignment has nothing to act on

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.

⚠️ markdown does 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>
PropertyTypeRole
pos size origin rotation rotationOriginpairplacement
maxSizepairmaximum size without distortion — see Position, size
keepratioyes/nokeep the proportions (⚠️ all lowercase)
pathpaththe image file
tileyes/norepeat the image as tiles instead of stretching it
colorcolortints the image (multiplication)
colorTop colorBottom colorLeft colorRightcolorgradient tint
colorTopLeft colorTopRight colorBottomLeft colorBottomRightcolorfour-corner tint
reflectionpaira reflection under the image: start and end opacity
zIndex disableddepth, 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:

PropertyTypeRole
delaynumberseconds before the video starts
loopsnumbernumber of plays; 0 = loop
animationstextthe appearance effect
linktexttie the playback to another component
reflectionpairreflection, as on image

video accepts neither tile nor the tint colors.

A delay of 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>
PropertyRole
pos size origin rotation rotationOriginplacement
colorsolid color
colorTop colorBottomvertical gradient
colorLeft colorRighthorizontal gradient
colorTopLeft colorTopRight colorBottomLeft colorBottomRightfour-corner gradient
zIndex disableddepth, 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. textlist only exists in the Games view, under the reserved name gamelist. 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>
PropertyRole
pos size originplacement
primaryColorthe color of the games
secondaryColorthe color of the folders
selectedColorthe text color of the chosen line
selectorColorthe color of the highlighter
selectorImagePatha highlighter image, instead of the color
selectorImageTilerepeat that image as tiles
selectorHeightthe highlighter's height
selectorOffsetYits vertical offset
fontPath fontSizethe font
alignmentline alignment
horizontalMarginthe left and right margin
forceUppercaseall capitals
lineSpacingline spacing — this is what spaces the lines
scrollSoundthe sound played while scrolling
zIndexdepth
Sonic the Hedgehog ▸ A folder Streets of Rage 2 Golden Axe Gunstar Heroes primaryColor the games secondaryColor the FOLDERS (not every other line!) selectorColor the highlighter selectedColor the TEXT of the chosen line selectorHeight the classic mistake: thinking secondaryColor alternates every other line

⚠️ 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.

type accepts horizontal (the default), vertical and vertical_wheel — the wheel. There is no horizontal wheel: any other value silently falls back to horizontal.

<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>
PropertyRole
typehorizontal, vertical, vertical_wheel
pos size originplacement
colorthe carousel's background
logoSizethe size of a logo (pair)
logoScalethe enlargement of the chosen logo
logoRotation logoRotationOriginlogo rotation (wheels)
logoAlignmentlogo alignment within their slot
maxLogoCounthow many logos visible at once
defaultTransitionfade or instant; any other value gives slide
fontPath fontSize fontColorthe text mode's font
forceUppercasesystem names all in capitals
textOnlywrite the names instead of the logos
primaryColor secondaryColorthe color of the names
selectedColorthe color of the chosen name
selectorColor selectorHeightthe text mode's highlighter
selectorOffsetX selectorOffsetYits offset
textOffsetXthe text offset
lineSpacing horizontalMarginline spacing and margins of the text mode
zIndexdepth
color — the carousel’s background chosen logoSize × logoScale maxLogoCount — how many are visible only the chosen logo is at full size: the others are shrunk

⚠️ 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.


In the System list section, “WHAT SCROLLS BY” picks between:

⚠️ 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.

In text mode, the font, its size and its color are set in the Text section. Two offsets specific to the carousel are added:

<carousel name="systemcarousel" type="vertical">
  <textOnly>true</textOnly>
  <textOffsetX>0.02</textOffsetX>
</carousel>

Available from Recalbox 10.1. On an older machine, textOnly is 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>
PropertyRole
pos size origin rotation rotationOriginplacement
filledPaththe filled star image
unfilledPaththe empty star image
zIndex disableddepth, 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 writeWhat shows
date1991/06/23
dateTime1991/06/23 14:05:30
year1991
time14:05:30
realTimethe 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)

PropertyRole
pos size originplacement
displaythe date's form
color backgroundColorcolors
fontPath fontSizefont
alignment forceUppercaseformatting
zIndex disableddepth, 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:

NameWhat the machine does
bgsoundplays that track
directorypicks 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

PropertyRole
pos sizeplacement
textColorthe labels' color
iconColorthe icons' tint — provide them in white
fontPath fontSizethe font

The 32 icons

Replacing an icon is optional: Recalbox provides its own. You only redefine the ones you want.

DirectionsiconUpDown, iconLeftRight, iconUpDownLeftRight

ButtonsiconA, iconB, iconX, iconY

TriggersiconL, iconR, iconL2, iconR2, iconL3, iconR3, iconLR, iconL2R2, iconL3R3

SystemiconStart, iconSelect, iconHotkey

Hotkey combinationsiconHkA, iconHkB, iconHkX, iconHkY, iconHkL, iconHkR, iconHkLeftRight

JoysticksiconJ1UpDown, 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.

iconHotkey counts 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:

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>

iconset is 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>
PropertyRole
colorthe frame's color — it tints the image if path is given
paththe frame's image
fadePaththe veil image that darkens the view behind

menuText (6) — the menu lines

PropertyRole
fontPath fontSizethe font
colorthe lines' text
selectedColorthe chosen line's text
selectorColorthe highlighter
separatorColorthe 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

⚠️ iconList is 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.

PropertyWhat it paints
keyColoreach key's background at rest
keySelectedColorthe key you are on
keyTextColorthe letter written on the key
keyDisabledColorthe letter of a character the input refuses
keyModifierColorShift / Ctrl / Alt pressed for a single key
keyModifierLockedColorShift / Ctrl / Alt locked
keyTitleColorthe title above the keyboard
keyEditTextColorthe text being typed
fontPaththe 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.

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:

  1. its reserved components, which it builds itself;
  2. the components marked extra="true".

A free component without extra never shows up.

extra="true" → drawn your component without extra → invisible nothing the engine only draws its reserved components + those marked extra
<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 · markdowntextlist · 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

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

NameTypeRole
systemcarouselcarouselthe carousel — one only, not renamable
logoimagethe system's logo in the carousel
systemInfotextthe “510 games available, 13 favorites” line
bgsoundsoundthe theme's background music
directorysoundthe theme's music folder

bgsound and directory are 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.

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.

PropertyDefaultDetail
typehorizontalhorizontal, vertical, vertical_wheel; any other value falls back to horizontal
logoSizecomputedratio of the screen, not of the carousel
logoScale1.2enlargement of the chosen logo
maxLogoCount3rounded to the integer — a decimal is useless
colortransparentthe 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 basic view is never requested by the engine, and the arcade view reuses detailed.

The list and the media

NameTypeRole
gamelisttextlistthe game list
logoimagethe system's logo
md_imageimagethe cover art
default_image_pathimagethe fallback image when the game has no cover
md_videovideothe preview video
md_region1md_region4imagethe game's four region flags

On md_image, the path written in the theme is ignored: the image comes from the game. On the md_region*, only pos, size, zIndex and path are read.

The game's information

NameType
md_descriptiontext, markdown or scrolltext — your choice
md_folder_nametext
md_ratingrating
md_releasedate, md_lastplayeddatetime
md_developer, md_publisher, md_genre, md_players, md_playcount, md_favoritetext

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 except text: their content comes from the game.

The list's colors

gamelist uses five colors, of which three are not themable:

LineColor
a gameprimaryColor
a foldersecondaryColor
a faded gamecomputed: primaryColor with the opacity halved
a faded foldercomputed the same way
the background of a sort headerimposed

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

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 menu is 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:

FamilyViews 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 gameits nameits release datethe rom file
a folderthe folder nameUNKNOWNthe folder name
a sort headernothingUNKNOWNnothing

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:

OldWhat it givesCurrent equivalent
$systemthe short name — “snes”${system.name}
$themethe 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 writeWhat it isWhat it returnsWhere
${system}System nameThe full name — e.g. “Sega Megadrive”Systems, Games
${system.input.keyboard}Keyboard needed?mandatory · recommended · optional · noSystems, Games
${system.input.mouse}Mouse needed?mandatory · recommended · optional · noSystems, Games
${system.input.pad}Controller needed?mandatory · recommended · optional · noSystems, Games
${system.logo}System logoThe path of the logo provided by RecalboxSystems, Games
${system.manufacturer}ManufacturerE.g. “Sega”, “Nintendo”. Empty if unknownSystems, Games
${system.name}Short system nameSystems, Games
${system.releasedate}Year of releaseYear and month — e.g. “1988-10”Systems, Games
${system.type}Machine type (technical name)arcade · console · handheld · computer · engine · port · fantasy · virtual · virtual-arcadeSystems, Games
${system.type.name}Machine typeThe same, in plain words: “Home Console”, “handheld Console”, “Arcade”…Systems, Games

The game

What to writeWhat it isWhat it returnsWhere
${game.developer}DeveloperE.g. “Konami”. “UNKNOWN” if absentScreensaver, Games
${game.file.name}File nameThe file name, extension includedScreensaver, Games
${game.file.path}Full file pathThe full path of the fileScreensaver, Games
${game.file.stem}File name (without extension)The file name, without the extensionGames, Screensaver
${game.genre.normalized}Genre (technical name)The normalized genre, in English — “Platform”, “Shoot’em Up”, “Racing”…Screensaver, Games
${game.genre.raw}GenreThe genre as written in the game's sheetScreensaver, Games
${game.isadult}Adults only?yes or noScreensaver, Games
${game.isfavorite}Is a favorite?yes or no (never true/false)Screensaver, Games
${game.ishidden}Is hidden?yes or noScreensaver, Games
${game.islastversion}Is the latest version?yes or noScreensaver, Games
${game.isnotagame}Is not a game?yes or noScreensaver, Games
${game.ispreinstalled}Is preinstalled?yes or noScreensaver, Games
${game.license}LicenseThe license, often emptyScreensaver, Games
${game.name}Game nameThe name of the gameScreensaver, Games
${game.players}Number of players“1”, “2”, “1-4”, “4+”…Screensaver, Games
${game.players.max}Players — maximumA number — e.g. “4”Screensaver, Games
${game.players.min}Players — minimumA number — e.g. “1”Screensaver, Games
${game.publisher}PublisherE.g. “Sega”. “UNKNOWN” if absentScreensaver, Games
${game.releasedate}Release dateISO date — e.g. “1991-06-23”. “UNKNOWN” if absentScreensaver, Games
${game.synopsis}DescriptionThe presentation text, often longGames, Screensaver

Rating and statistics

What to writeWhat it isWhat it returnsWhere
${game.lastplayed}Last playedISO date, or “NEVER” if never playedScreensaver, Games
${game.rating.10}Rating (out of 10)An integer from 0 to 10Screensaver, Games
${game.rating.100}Rating (out of 100)An integer from 0 to 100Screensaver, Games
${game.rating.5}Rating (out of 5)An integer from 0 to 5 — not starsScreensaver, Games
${game.timesplayed}Number of playsA number of playsScreensaver, Games
${game.totalplayed}Total play timeA duration — e.g. “3h 12m”. “NONE” if zeroScreensaver, Games

The game's images and video

What to writeWhat it isWhat it returnsWhere
${game.media.boxpath}Box artGames, Screensaver
${game.media.imagepath}Cover art / imageThe path of the cover art. Empty if the game has none — see ifexistsScreensaver, Games
${game.media.thumbpath}ThumbnailThe path of the thumbnailScreensaver, Games
${game.media.videopath}VideoThe path of the videoScreensaver, Games

The game's medium

What to writeWhat it isWhat it returnsWhere
${game.support.index}Medium indexThe disc number — empty if there is only oneScreensaver, Games
${game.support.number}Medium numberThe whole thing assembled — e.g. “2A/3”Screensaver, Games
${game.support.side}Medium sideThe side of the medium — A, B…Screensaver, Games
${game.support.total}Number of mediaThe number of media. “UNKNOWN” if unknownGames, Screensaver
${game.support.type}Medium typeCartridge · CD/DVD · Harddisk · Files · Tape · Quick Disc · 3" Floppy · 3".5 Floppy · 5".25 Floppy · PCB · UnknownScreensaver, Games

The game's system

What to writeWhat it isWhat it returnsWhere
${game.system}Name of the game's systemThe full name of the game's systemScreensaver, Games
${game.system.input.keyboard}Keyboard required by the game's systemmandatory · recommended · optional · noScreensaver, Games
${game.system.input.mouse}Mouse required by the game's systemmandatory · recommended · optional · noScreensaver, Games
${game.system.input.pad}Controller required by the game's systemmandatory · recommended · optional · noScreensaver, Games
${game.system.logo}Logo of the game's systemThe path of its logoScreensaver, Games
${game.system.manufacturer}Manufacturer of the game's systemIts manufacturerScreensaver, Games
${game.system.name}Short name of the game's systemIts internal nameScreensaver, Games
${game.system.releasedate}Year of the game's systemIts year of releaseScreensaver, 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 systemThe same, in plain wordsScreensaver, Games

Emulator

What to writeWhat it isWhat it returnsWhere
${game.emulator.compatibility}Compatibilityunknown · low · average · good · high · perfectScreensaver, Games
${game.emulator.extensions}Supported extensionsThe supported extensions — e.g. “.bin .gen .md”Screensaver, Games
${game.emulator.hasnetplay}Supports online play?yes or noScreensaver, Games
${game.emulator.hassoftpatching}Accepts patches?yes or noGames, Screensaver
${game.emulator.islibretro}Is a Libretro core?yes or noScreensaver, Games
${game.emulator.name}Emulator nameE.g. “libretro picodrive”Screensaver, Games
${game.emulator.speed}Speedunknown · low · average · good · high · perfectScreensaver, Games

The machine and its settings

What to writeWhat it isWhat it returnsWhere
${display.overscan}Overscan?yes or noScreensaver, Systems, Menu, Games
${random.between(a,b,c)}A random value among…one of the given valueseverywhere
${random.range(1,10)}A random number between…an integer between the two boundseverywhere
${display.resolution}Resolutionfhd (1080p and above) · hd (720p) · vga · qvgaSystems, Menu, Games, Screensaver
${display.tate}Vertical screen (TATE)?yes or noScreensaver, Systems, Menu, Games
${display.tateleft}Rotated to the left?yes or noScreensaver, Systems, Menu, Games
${display.tateright}Rotated to the right?yes or noMenu, Games, Screensaver, Systems
${hardware.board}Machine modelThe model — “RPi 5”, “PC x64”, “RG351P/M”…Screensaver, Systems, Menu, Games
${hardware.crt}CRT screen?yes or noScreensaver, Systems, Menu, Games
${hardware.isanbernic}Is it an Anbernic?yes or noScreensaver, Systems, Menu, Games
${hardware.isodroid}Is it an Odroid?yes or noScreensaver, Systems, Menu, Games
${hardware.ispc}Is it a PC?yes or noSystems, Menu, Games, Screensaver
${hardware.ispi}Is it a Raspberry Pi?yes or noScreensaver, Systems, Menu, Games
${hardware.jamma}Jamma cabinet?yes or noScreensaver, Systems, Menu, Games
${recalbox.built}Build dateThe build dateMenu, Games, Screensaver, Systems
${recalbox.version}Recalbox versionE.g. “10.0”Screensaver, Systems, Menu, Games
${root}Theme folderThe root of the selected theme — to put in front of all your pathsScreensaver, Systems, Menu, Games
${settings.language}LanguageThe language alone — e.g. “fr”Screensaver, Systems, Menu, Games
${settings.locale}Language and countryLanguage and country — e.g. “fr_FR”Screensaver, Systems, Menu, Games
${settings.region}Chosen regioneu · us · jpSystems, Menu, Games, Screensaver
${settings.timezone}Time zoneE.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

  1. open Variables in the toolbar: the panel opens on the right;
  2. + 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);
  3. 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

ConditionTrue when…
hd fhd vga qvgathe screen has that resolution
crtthe screen is a CRT tube
overscanthe picture overscans (CRT without Jamma)
tate tateleft taterightthe screen is vertical
jamma bartopthe machine is an arcade cabinet
ispc ispi isodroid isanbernicthe machine is that model
nomenumenus 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.

ConditionTrue when…
console handheld computer arcade engine port fantasythe system is of that type
virtualit is an automatic system (Favorites, Last played, All games…)
favoriteit is the automatic Favorites system
lastplayedit is the automatic Last played system

⚠️ favorite does 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}) or ifexists.


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:

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 meanSignSpelled 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 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}
240ptestsuite240ptestsuite240ptestsuitevirtualvirtual
Acorn BBC MicrobbcmicroAcorn BBC MicroAcorncomputer
Amiga AGAamiga1200Amiga AGACommodorecomputer
Amiga CD32amigacd32Amiga CD32Commodoreconsole
Amiga CDTVamigacdtvAmiga CDTVCommodoreconsole
Amiga ECS/OCSamiga600Amiga ECS/OCSCommodorecomputer
Amstrad GX4000gx4000Amstrad GX4000Amstradconsole
AmstradCPCamstradcpcAmstradCPCAmstradcomputer
Apple IIapple2Apple IIApplecomputer
Apple IIGSapple2gsApple IIGSApplecomputer
Apple MacintoshmacintoshApple MacintoshApplecomputer
ArduboyarduboyArduboyfantasyfantasy
Atari 2600atari2600Atari 2600Atariconsole
Atari 5200atari5200Atari 5200Atariconsole
Atari 7800atari7800Atari 7800Atariconsole
Atari 8bitsatari800Atari 8bitsAtaricomputer
Atari JaguarjaguarAtari JaguarAtariconsole
Atari STataristAtari STAtaricomputer
ColecovisioncolecovisionColecovisionColecoconsole
Commodore 64c64Commodore 64Commodorecomputer
Commodore VIC-20vic20Commodore VIC-20Commodorecomputer
DaphnedaphneDaphneDaphnearcade
DICEdiceDICEDICEarcade
Dos (x86)dosDos (x86)IBMcomputer
Dragon 32/64dragonDragon 32/64DragonDatacomputer
EasyRPGeasyrpgEasyRPGvirtualengine
Elektronika BKbkElektronika BKElektronikacomputer
Epoch Cassette VisioncassettevisionEpoch Cassette VisionEpochconsole
Exelvision EXL 100exl100Exelvision EXL 100Exelvisioncomputer
Fairchild Channel FchannelfFairchild Channel FFairchildconsole
Family Computer Disk SystemfdsFamily Computer Disk SystemNintendoconsole
FinalBurn NeofbneoFinalBurn NeoFBNarcade
Game and WatchgwGame and WatchNintendohandheld
Game BoygbGame BoyNintendohandheld
Game Boy AdvancegbaGame Boy AdvanceNintendohandheld
Game Boy ColorgbcGame Boy ColorNintendohandheld
GameCubegamecubeGameCubeNintendoconsole
Infocom Z-MachinezmachineInfocom Z-MachineInfocomengine
LowRes NXlowresnxLowRes NXvirtualfantasy
LutrolutroLutrovirtualfantasy
LynxlynxLynxAtarihandheld
MamemameMameMamearcade
Mattel IntellivisionintellivisionMattel IntellivisionMattelconsole
MegaDuckmegaduckMegaDuckWelbackhandheld
MGT SAM CoupésamcoupeMGT SAM CoupéMGTcomputer
MoonlightmoonlightMoonlightNVidiavirtual
MSX1msx1MSX1Microsoftcomputer
MSX2msx2MSX2Microsoftcomputer
MSXturboRmsxturborMSXturboRMicrosoftcomputer
NEC PC-88pc88NEC PC-88NECcomputer
NEC PC-98pc98NEC PC-98NECcomputer
NEC PC-FXpcfxNEC PC-FXNECconsole
Neo-Geo AESneogeoNeo-Geo AESSNKconsole
Neo-Geo CDneogeocdNeo-Geo CDSNKconsole
Neo-Geo PocketngpNeo-Geo PocketSNKhandheld
Neo-Geo Pocket ColorngpcNeo-Geo Pocket ColorSNKhandheld
Nintendo 64n64Nintendo 64Nintendoconsole
Nintendo 64DD64ddNintendo 64DDNintendoconsole
Nintendo DSndsNintendo DSNintendohandheld
Nintendo Entertainment SystemnesNintendo Entertainment SystemNintendoconsole
Odyssey2o2emOdyssey2Magnavoxconsole
OpenBORopenborOpenBORSenile Teamengine
Oric/AtmosoricatmosOric/AtmosTangerinecomputer
Othello MultivisionmultivisionOthello MultivisionTsukudaconsole
PalmpalmPalmPalmhandheld
Panasonic 3DO3doPanasonic 3DOPanasonicconsole
PC EnginepcenginePC EngineNECconsole
PC Engine CDpcenginecdPC Engine CDNECconsole
Philips CD-IcdiPhilips CD-IPhillipsconsole
Philips P2000Tp2000tPhilips P2000TPhilipscomputer
Philips VG 5000vg5000Philips VG 5000Philipscomputer
PICO-8pico8PICO-8virtualfantasy
Pocket Challenge v2pcv2Pocket Challenge v2Benessehandheld
Pokémon MinipokeminiPokémon MiniNintendohandheld
Sammy AtomiswaveatomiswaveSammy AtomiswaveSammyarcade
SatellaviewsatellaviewSatellaviewNintendoconsole
ScreenshotsimageviewerScreenshotsvirtualvirtual
ScummVMscummvmScummVMLudvig Strigeusengine
Sega 32Xsega32xSega 32XSegaconsole
Sega CDsegacdSega CDSegaconsole
Sega DreamcastdreamcastSega DreamcastSegaconsole
Sega Game GeargamegearSega Game GearSegahandheld
Sega Master System / Mark IIImastersystemSega Master System / Mark IIISegaconsole
Sega MegadrivemegadriveSega MegadriveSegaconsole
Sega Model3model3Sega Model3Segaarcade
Sega NAOMInaomiSega NAOMISegaarcade
Sega NAOMI 2naomi2Sega NAOMI 2Segaarcade
Sega NAOMI GD-ROM SystemnaomigdSega NAOMI GD-ROM SystemSegaarcade
Sega PicopicoSega PicoSegaconsole
Sega SaturnsaturnSega SaturnSegaconsole
Sega SG1000sg1000Sega SG1000Segaconsole
Sharp X1x1Sharp X1Sharpcomputer
Sharp X68000x68000Sharp X68000Sharpcomputer
SolarussolarusSolarusSolarusengine
Sony Playstation 1psxSony Playstation 1Sonyconsole
Sony Playstation 2ps2Sony Playstation 2Sonyconsole
Sony Playstation PortablepspSony Playstation PortableSonyhandheld
SpectravideospectravideoSpectravideoSpectravideocomputer
ST-VstvST-VSegaarcade
SuFami TurbosufamiSuFami TurboBandaiconsole
Super Cassette VisionscvSuper Cassette VisionEpochconsole
Super Nintendo Entertainment SystemsnesSuper Nintendo Entertainment SystemNintendoconsole
SupergrafxsupergrafxSupergrafxNECconsole
Texas Instrument TI-99/4Ati994aTexas Instrument TI-99/4ATexas Instrumentcomputer
ThomsonthomsonThomsonThomsoncomputer
TIC-80tic80TIC-80portfantasy
TRS-80 Color Computertrs80cocoTRS-80 Color ComputerTandycomputer
UzeboxuzeboxUzeboxportconsole
VectrexvectrexVectrexMBconsole
Videopac+ G7400videopacplusVideopac+ G7400Philipsconsole
Vircon32vircon32Vircon32virtualconsole
Virtual BoyvirtualboyVirtual BoyNintendoconsole
Visual Pinball StandalonevpinballVisual Pinball StandaloneRandy Davisengine
WASM-4wasm4WASM-4Bruno Garciafantasy
Watara SupervisionsupervisionWatara SupervisionWatarahandheld
WiiwiiWiiNintendoconsole
WonderSwanwswanWonderSwanBandaihandheld
WonderSwan ColorwswancWonderSwan ColorBandaihandheld
XboxxboxXboxMicrosoftconsole
ZX81zx81ZX81Sinclaircomputer
ZXSpectrumzxspectrumZXSpectrumSinclaircomputer

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)dospc
GameCubegamecubegc
Odyssey2o2emodyssey2
Oric/Atmosoricatmosoric
Thomsonthomsonto8
WonderSwanwswanwonderswan
WonderSwan Colorwswancwonderswancolor

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}
PortsportsPortsvirtual
FavoritesfavoritesFavoritesvirtual
Last playedlastplayedLast playedvirtual
All gamesallgamesAll gamesvirtual
MultiplayermultiplayerMultiplayervirtual
ArcadearcadeArcadevirtual-arcade
LightgunlightgunLightgunvirtual
TatetateTatevirtual
DialdialDialvirtual
TrackballtrackballTrackballvirtual
ChallengeschallengesChallengesvirtual

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 playedlastplayedauto-lastplayed
All gamesallgamesauto-allgames
Multiplayermultiplayerauto-multiplayer
Lightgunlightgunauto-lightgun
Tatetateauto-tate
Dialdialauto-dial
Trackballtrackballauto-trackball
Challengeschallengesauto-challenges

Arcade by manufacturer (54) (${system.type} = virtual-arcade)

System${system.name}${system}
Acclaimarcade-manufacturer-acclaimAcclaim
Atariarcade-manufacturer-atariAtari
Atlusarcade-manufacturer-atlusAtlus
Banprestoarcade-manufacturer-banprestoBanpresto
Capcom Cps1arcade-manufacturer-capcom-cps1Capcom Cps1
Capcom Cps2arcade-manufacturer-capcom-cps2Capcom Cps2
Capcom Cps3arcade-manufacturer-capcom-cps3Capcom Cps3
Capcomarcade-manufacturer-capcomCapcom
Cavearcade-manufacturer-caveCave
Data eastarcade-manufacturer-data eastData east
Exidyarcade-manufacturer-exidyExidy
Hng64arcade-manufacturer-hng64Hng64
Igsarcade-manufacturer-igsIgs
Irem M72arcade-manufacturer-irem-m72Irem M72
Irem M92arcade-manufacturer-irem-m92Irem M92
Iremarcade-manufacturer-iremIrem
Itecharcade-manufacturer-itechItech
Jalecoarcade-manufacturer-jalecoJaleco
Kanekoarcade-manufacturer-kanekoKaneko
Konami Gxarcade-manufacturer-konami-gxKonami Gx
Konamiarcade-manufacturer-konamiKonami
Midwayarcade-manufacturer-midwayMidway
Mitchellarcade-manufacturer-mitchellMitchell
Namco Naarcade-manufacturer-namco-naNamco Na
Namco Nbarcade-manufacturer-namco-nbNamco Nb
Namco System1arcade-manufacturer-namco-system1Namco System1
Namco System10arcade-manufacturer-namco-system10Namco System10
Namco System11arcade-manufacturer-namco-system11Namco System11
Namco System12arcade-manufacturer-namco-system12Namco System12
Namco System18arcade-manufacturer-namco-system18Namco System18
Namco System2arcade-manufacturer-namco-system2Namco System2
Namcoarcade-manufacturer-namcoNamco
Neogeoarcade-manufacturer-neogeoNeogeo
Nichibutsuarcade-manufacturer-nichibutsuNichibutsu
Nintendoarcade-manufacturer-nintendoNintendo
Nmkarcade-manufacturer-nmkNmk
Psikyoarcade-manufacturer-psikyoPsikyo
Raizingarcade-manufacturer-raizingRaizing
Sammyarcade-manufacturer-sammySammy
Sega Stvarcade-manufacturer-sega-stvSega Stv
Sega System16arcade-manufacturer-sega-system16Sega System16
Sega System18arcade-manufacturer-sega-system18Sega System18
Sega System32arcade-manufacturer-sega-system32Sega System32
Segaarcade-manufacturer-segaSega
Seibuarcade-manufacturer-seibuSeibu
Setaarcade-manufacturer-setaSeta
Snkarcade-manufacturer-snkSnk
Taito F3arcade-manufacturer-taito-f3Taito F3
Taito Gnetarcade-manufacturer-taito-gnetTaito Gnet
Taitoarcade-manufacturer-taitoTaito
Technosarcade-manufacturer-technosTechnos
Tecmoarcade-manufacturer-tecmoTecmo
Toaplanarcade-manufacturer-toaplanToaplan
Viscoarcade-manufacturer-viscoVisco

By genre (56) (${system.type} = virtual)

System${system.name}${system}
Actiongenre-actionAction
Platformgenre-actionplatformerPlatform
Platform Shootergenre-actionplatformshooterPlatform Shooter
First Person Shootergenre-actionfirstpersonshooterFirst Person Shooter
Shoot'em Upgenre-actionshootemupShoot'em Up
Shoot with Gungenre-actionshootwithgunShoot with Gun
Fightinggenre-actionfightingFighting
Beat'em Allgenre-actionbeatemupBeat'em All
Infiltrationgenre-actionstealthInfiltration
Battle Royalegenre-actionbattleroyaleBattle Royale
Rythm & Musicgenre-actionrythmRythm & Music
Adventuregenre-adventureAdventure
Textual Adventuregenre-adventuretextTextual Adventure
Graphical Adventuregenre-adventuregraphicsGraphical Adventure
Visual Novelgenre-adventurevisualnovelsVisual Novel
Interactive Moviegenre-adventureinteractivemovieInteractive Movie
Real Time 3D Adventuregenre-adventurerealtime3dReal Time 3D Adventure
Survivalgenre-adventuresurvivalhorrorSurvival
RPGgenre-rpgRPG
Action RPGgenre-rpgactionAction RPG
MMORPGgenre-rpgmmoMMORPG
Dungeon Crawlergenre-rpgdungeoncrawlerDungeon Crawler
Tactical RPGgenre-rpgtacticalTactical RPG
JRPGgenre-rpgjapaneseJRPG
Party based RPGgenre-rpgfirstpersonpartybasedParty based RPG
Simulationgenre-simulationSimulation
Build & Managementgenre-simulationbuildandmanagementBuild & Management
Life Simulationgenre-simulationlifeLife Simulation
Fishing & Huntinggenre-simulationfishandhuntFishing & Hunting
Vehicle Simulationgenre-simulationvehicleVehicle Simulation
Science Fiction Simulationgenre-simulationscifiScience Fiction Simulation
Strategygenre-strategyStrategy
eXplore, eXpand, eXploit & eXterminategenre-strategy4xeXplore, eXpand, eXploit & eXterminate
Artillerygenre-strategyartilleryArtillery
Auto-battlergenre-strategyautobattlerAuto-battler
Multiplayer Online Battle Arenagenre-strategymobaMultiplayer Online Battle Arena
Real Time Strategygenre-strategyrtsReal Time Strategy
Turn Based Strategygenre-strategytbsTurn Based Strategy
Tower Defensegenre-strategytowerdefenseTower Defense
Wargamegenre-strategywargameWargame
Sportsgenre-sportsSports
Racinggenre-sportracingRacing
Sport Simulationgenre-sportsimulationSport Simulation
Competition Sportgenre-sportcompetitiveCompetition Sport
Fighting/Violent Sportgenre-sportfightFighting/Violent Sport
Pinballgenre-pinballPinball
Board gamegenre-boardBoard game
Casual gamegenre-casualCasual game
Digital Cardsgenre-digitalcardDigital Cards
Puzzle & Logicgenre-puzzleandlogicPuzzle & Logic
Multiplayer Party Gamegenre-partyMultiplayer Party Game
Triviagenre-triviaTrivia
Casinogenre-casinoCasino
Multi Game Compilationgenre-compilationMulti Game Compilation
Demo from Demo Screnegenre-demosceneDemo from Demo Screne
Educativegenre-educativeEducative

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.

FontWhat 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:

OptionWhat it changes
systemViewthe layout of the system list
gameListthat of the game list
gameclipviewthat of the screensaver
SysInfosthe systems' information: full, minimal, hidden
gameInfosthe games' information
colorTheme12 palettes of colors
shaderthe retro filter: CRT, scanlines, honeycomb, none
shadowthe shading
bandsthe color bands: thin, thick, none
iconesetThemethe 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.

<subset subset="colorTheme" declares the GROUP + its title <include … name="1 - Blue"> <include … name="2 - Red"> each choice = a file loaded on top Settings ▸ Theme Theme colors ◁ Blue ▷ what the user sees on their machine the “1 - ” prefix is there to ORDER the choices: it is not displayed

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" />
AttributeRole
subsetthe identifier of the group — it is what ties the choices together
titlethe label the user reads in the menu
helpthe 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>
AttributeRole
subsetwhich group this choice belongs to
namethe 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:

  1. if ALL the choices have a number at the head of their name, it sorts by that number;
  2. 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.

  1. Export the theme from the studio;
  2. copy the folder into /recalbox/share/themes/;
  3. 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 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:

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

⚠️ 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

  1. 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;
  2. an administrator decides. The vote informs, it does not decide: no one is published by a tally;
  3. 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