A theme is a CSS file. It sets a list of custom properties - tokens - that the shell reads. There is no scripting, no layout, no selectors of your own: a theme can change how ApolloShell looks, and nothing else.
The format is built to survive. A theme written today keeps working when the shell grows twenty new features, because the rules below are promises, not current behaviour.
~/Library/Application Support/ApolloShell/themes/
├── Minimal.css ← a theme in a single file
└── Everything/ ← a theme with files of its own
├── theme.css ← must be called exactly this
├── background.png
└── author.png
A theme is either
- a single
Name.css, or - a folder
Name/containingtheme.css.
The file or folder name is the theme's identifier. It is what the shell remembers when you pick a theme, so renaming the file means picking the theme again.
Only a folder theme can use images. A single file has no folder of its own, and letting it reach into the themes folder would let it read its neighbours.
/* Comments are allowed anywhere. */
:root {
--apollo-accent-color: #ff6b35;
--apollo-corner-radius: 6px;
}
@media (prefers-color-scheme: dark) {
:root {
--apollo-accent-color: #ff8354;
}
}:root holds the theme. The @media (prefers-color-scheme: dark) block holds
only the values that differ in dark mode; everything else is inherited from
:root, exactly like in a browser. Tokens you do not mention keep their
built-in value - and the built-in value already differs between light and
dark, so a theme that only sets an accent colour still looks right in both.
Read:
:root { … }blocks, as many as you like; the last value for a token wins@media (prefers-color-scheme: dark) { :root { … } }/* comments */!important(ignored, but tolerated)
Silently skipped:
- any other selector (
h1,:root:hover,*) - any other at-rule, including
@import,@supportsand other@mediaqueries - a theme never loads a second file - ordinary properties in
:root, such ascolor: red - custom properties outside our namespace, such as
--my-blue
Not supported, on purpose: var(), calc, nesting, and anything that needs a
lookup chain. A value that contains var( counts as unreadable.
Everything that gets skipped, and every value that cannot be read, is reported as a note with a line number. Notes never stop a theme from loading.
| Type | Accepted | Examples |
|---|---|---|
| color | #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(), rgba(), hsl(), hsla(), basic colour names, transparent |
#ff6b35, rgb(255 107 53 / 80%), hsl(20, 100%, 60%), red |
| gradient | none, or linear-gradient(<angle>, <colour> <position>, …) |
linear-gradient(180deg, #101014, #2a2a33) |
| length | a number with px, pt or no unit; px and pt are the same here, the shell works in points |
12px, 12, 0.5px |
| ratio | a number from 0 to 1, or a percentage | 0.5, 50% |
| number | a plain number, no unit | 400 |
| text | a quoted string, or the text as written | "Alex", Inter |
| file | url("name.png") or a quoted name, relative to the theme folder; none for nothing |
url("background.png") |
| option | one of the listed words | fill |
| flag | true/false, yes/no, on/off, 1/0 |
true |
Colours are stored with 8 bits per channel, the same precision they are written with. Token names are matched case-insensitively, even though CSS custom properties are normally case-sensitive.
Every gradient token sits next to a colour token and is none by default,
so a theme that only sets colours looks exactly as it did before. Set the
gradient and it paints that surface instead of the flat colour; the colour
still decides how text on it is checked for contrast.
--apollo-bar-gradient: linear-gradient(180deg, #101014 0%, #2a2a33 100%);- Only
linear-gradientexists.radial-gradientand friends are not read; a theme that uses one gets a note and the flat colour. - The angle follows CSS:
0degpoints up,90degto the right. Words work too -to bottom,to top right. Leave it out and it runs top to bottom. - Between two and eight colour stops. Positions are percentages and may be left out, and the colours are then spread evenly. A position that would step backwards is pulled up to the one before it.
- A gradient never makes a surface disappear: colours are clamped like every
other colour, and
noneis always a valid value.
Every token and its type. A token your theme does not mention changes nothing - that part of the shell stays exactly as it looks without a theme, including the materials and the sizes you set in Nexus. The Default column is what that built-in look amounts to, so you can start from it. Dark is the built-in value in dark mode, where it differs. Numbers are clamped to the range in the type column.
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-theme-format |
number (1–1000000) | 1 |
Format the theme was written for; higher numbers still load | |
--apollo-theme-name |
text | "" |
Name shown in the theme list; empty means the file or folder name | |
--apollo-theme-author |
text | "" |
Who made the theme | |
--apollo-theme-description |
text | "" |
One line about the theme | |
--apollo-theme-version |
text | "" |
Version of the theme itself, free text | |
--apollo-theme-homepage |
text | "" |
Where the theme comes from; never opened or fetched by the core | |
--apollo-theme-author-image |
file | none |
Picture of the author, a file inside the theme folder | |
--apollo-theme-appearance |
option (auto, light, dark) | auto |
Which appearance the theme is made for; auto follows the system |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-background-color |
color | #e8e8ed |
#101014 |
Desktop backdrop behind the shell |
--apollo-background-image |
file | none |
Image behind the shell, a file inside the theme folder | |
--apollo-background-image-opacity |
ratio (0–1) | 1 |
How strongly the background image shows | |
--apollo-background-fit |
option (fill, fit, stretch, tile, center) | fill |
How the background image is placed | |
--apollo-surface-color |
color | #ffffff |
#1c1c1e |
Base surface of windows and popovers |
--apollo-surface-opacity |
ratio (0–1) | 1 |
How opaque surfaces are | |
--apollo-background-gradient |
gradient | none |
Gradient behind the shell instead of the flat backdrop colour | |
--apollo-surface-gradient |
gradient | none |
Gradient across surfaces instead of the flat surface colour | |
--apollo-elevated-surface-color |
color | #f5f5f7 |
#2a2a2d |
Surface of things that sit on top, such as menus |
--apollo-separator-color |
color | #d8d8dc |
#3a3a3d |
Hairlines between rows and sections |
--apollo-border-color |
color | #d0d0d4 |
#3f3f43 |
Outline around surfaces |
--apollo-border-width |
length (0px–8px) | 1px |
Thickness of that outline | |
--apollo-shadow-opacity |
ratio (0–1) | 0.18 |
0.45 |
How dark shadows under surfaces are |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-text-color |
color | #1c1c1e |
#f5f5f7 |
Main text |
--apollo-secondary-text-color |
color | #6b6b70 |
#aeaeb2 |
Subtitles and captions |
--apollo-muted-text-color |
color | #8e8e93 |
Text that should step back, such as hints | |
--apollo-link-color |
color | #0060df |
#6cb6ff |
Links |
--apollo-on-accent-color |
color | #ffffff |
Text and glyphs on accent coloured areas |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-accent-color |
color | #007aff |
#0a84ff |
Colour of selected and active things |
--apollo-secondary-accent-color |
color | #5e5ce6 |
#7d7aff |
Second accent for charts and badges |
--apollo-accent-gradient |
gradient | none |
Gradient for accent coloured areas instead of the flat accent colour | |
--apollo-selection-color |
color | #d6e4ff |
#234a77 |
Background of a selected row |
--apollo-hover-color |
color | #00000014 |
#ffffff1a |
Tint under the pointer |
--apollo-success-color |
color | #34c759 |
#30d158 |
Everything is fine |
--apollo-warning-color |
color | #c77700 |
#ffd60a |
Something needs attention |
--apollo-danger-color |
color | #d70015 |
#ff453a |
Something went wrong |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-bar-color |
color | #f5f5f7 |
#1c1c1e |
Backing of the sidebar |
--apollo-bar-gradient |
gradient | none |
Gradient along the sidebar instead of the flat bar colour | |
--apollo-bar-opacity |
ratio (0–1) | 1 |
How opaque that backing is | |
--apollo-bar-text-color |
color | #1c1c1e |
#f5f5f7 |
Text in the sidebar, such as the clock |
--apollo-bar-icon-color |
color | #3c3c43 |
#e5e5ea |
Status glyphs in the sidebar |
--apollo-bar-width |
length (36px–160px) | 44px |
Width of the sidebar | |
--apollo-bar-radius |
length (0px–48px) | 16px |
Corner radius of the sidebar | |
--apollo-bar-padding |
length (0px–48px) | 10px |
Space between sidebar edge and its blocks | |
--apollo-bar-item-spacing |
length (0px–48px) | 8px |
Space between two blocks | |
--apollo-bar-blur |
length (0px–64px) | 24px |
Blur behind the sidebar |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-dock-icon-size |
length (16px–128px) | 26px |
Size of the app icons | |
--apollo-dock-spacing |
length (0px–48px) | 4px |
Space between two app icons | |
--apollo-dock-indicator-color |
color | #8e8e93 |
#aeaeb2 |
Dot under a running app |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-panel-color |
color | #ffffff |
#1e1e20 |
Backing of dashboard, utilities and popovers |
--apollo-panel-opacity |
ratio (0–1) | 1 |
How opaque panels are | |
--apollo-panel-radius |
length (0px–48px) | 20px |
Corner radius of panels | |
--apollo-panel-padding |
length (0px–64px) | 16px |
Space inside a panel | |
--apollo-panel-blur |
length (0px–64px) | 24px |
Blur behind panels | |
--apollo-card-color |
color | #f2f2f7 |
#2a2a2d |
Backing of a card in a panel |
--apollo-panel-gradient |
gradient | none |
Gradient across a panel instead of the flat panel colour | |
--apollo-card-gradient |
gradient | none |
Gradient across a card instead of the flat card colour | |
--apollo-card-radius |
length (0px–48px) | 14px |
Corner radius of a card |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-launcher-highlight-color |
color | #e5efff |
#2a3c55 |
Backing of the selected launcher row |
--apollo-launcher-highlight-gradient |
gradient | none |
Gradient behind the selected launcher row instead of the flat colour | |
--apollo-launcher-row-height |
length (24px–96px) | 44px |
Height of one launcher row |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-font-family |
text | "" |
Font for the whole shell; empty means the system font | |
--apollo-monospace-font-family |
text | "" |
Font for numbers and code; empty means the system font | |
--apollo-font-size |
length (8px–32px) | 13px |
Base text size | |
--apollo-font-weight |
number (100–900) | 400 |
Base text weight |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-corner-radius |
length (0px–48px) | 12px |
Corner radius of everything without its own | |
--apollo-control-radius |
length (0px–48px) | 8px |
Corner radius of buttons and fields | |
--apollo-spacing |
length (0px–64px) | 12px |
Base spacing between elements | |
--apollo-animation-speed |
number (0–3) | 1 |
Factor on every animation; 0 means no animation | |
--apollo-animations |
flag | true |
Whether the shell animates at all | |
--apollo-glass |
flag | true |
Whether Liquid Glass is used where it fits | |
--apollo-shadows |
flag | true |
Whether surfaces cast a shadow | |
--apollo-icon-style |
option (auto, monochrome, colorful) | auto |
How status glyphs are drawn |
| Token | Type | Default | Dark | What it does |
|---|---|---|---|---|
--apollo-toast-color |
color | #1c1c1e |
#f5f5f7 |
Backing of a toast |
--apollo-toast-gradient |
gradient | none |
Gradient across a toast instead of the flat toast colour | |
--apollo-toast-text-color |
color | #ffffff |
#1c1c1e |
Text in a toast |
--apollo-toast-radius |
length (0px–48px) | 14px |
Corner radius of a toast |
A theme folder may carry an icons/ folder. The file name is the icon it
replaces, without the extension:
Nightfall/
├── theme.css
└── icons/
├── session-shutdown.png
└── bar-power.png
What is not in there stays the built-in SF Symbol, so a theme can replace one
icon or all of them. By default an image is shown exactly as it was drawn,
colours and all. Set --apollo-icon-style: monochrome and the images are
tinted like the symbols they replace instead, which is what a single-colour
set usually wants; auto and colorful leave them alone. File names are matched case-insensitively, a name this
version does not know is ignored and listed in Nexus, and the same rules as
for every other image apply: inside the theme folder, a supported type, and
within the size limit.
File in icons/ |
Replaces | What it is |
|---|---|---|
bar-dashboard |
square.grid.2x2.fill |
Opens the dashboard |
bar-utilities |
slider.horizontal.3 |
Opens the control centre |
bar-clock |
calendar |
Above the clock in the bar |
bar-power |
power |
Opens the session menu |
bar-launcher |
magnifyingglass |
Opens the launcher |
status-wifi |
wifi |
Wi-Fi, when it is connected |
status-wifi-off |
wifi.slash |
Wi-Fi, when it is off |
status-bluetooth |
bluetooth |
Bluetooth, when it is on |
status-bluetooth-off |
bluetooth.slash |
Bluetooth, when it is off |
status-battery |
battery.100percent |
Battery |
status-battery-charging |
battery.100percent.bolt |
Battery while charging |
status-volume |
speaker.wave.2.fill |
Volume |
status-volume-muted |
speaker.slash.fill |
Volume, when it is muted |
session-emblem |
the drawn emblem | The emblem in the middle of the session menu |
session-logout |
rectangle.portrait.and.arrow.right |
Log out |
session-sleep |
moon.fill |
Sleep |
session-restart |
arrow.clockwise |
Restart |
session-shutdown |
power |
Shut down |
toast-info |
info.circle.fill |
A toast that just says something |
toast-success |
checkmark.circle.fill |
A toast about something that worked |
toast-warning |
exclamationmark.triangle.fill |
A toast that warns |
toast-error |
exclamationmark.circle.fill |
A toast about a failure |
panel-media |
music.note |
Media, and the placeholder without artwork |
panel-performance |
speedometer |
The performance tab |
panel-weather |
cloud.sun.fill |
The weather tab |
panel-cpu |
cpu |
Processor load |
panel-memory |
memorychip |
Memory in use |
panel-disk |
internaldrive |
Disk in use |
These are the rules ApolloShell holds itself to. Tests enforce each of them.
- A published token name never disappears. If a token is renamed, the old name stays valid forever as an alias.
- New tokens only get added. Their default is always the way the shell looks today, so an old theme that never heard of them looks unchanged.
- An unknown token is ignored, not an error. It only produces a note. This is what makes a theme from a newer version usable on an older shell.
- A missing token falls back to its default, per appearance.
- An unreadable value falls back to the default (or to the last readable value of the same token) and produces a note with the line number.
- A broken file is still a theme. Empty, truncated, binary, wrong encoding, far too large: the theme loads with defaults and a note. Nothing ever throws, nothing ever crashes.
--apollo-theme-formatis informational. A theme that names a higher format than the shell knows is still read: known tokens apply, unknown ones are ignored, and a note says so. It is never rejected. The number only goes up if the meaning of an existing token changes - adding tokens does not change it.
A theme is a file from the internet. It is treated like one.
- Images only from the theme's own folder. Relative paths, no
.., no absolute paths, no~, nohttp(s), nofile:, nodata:. Percent escapes are decoded before the check, so%2e%2edoes not slip through. - Symlinks are resolved and checked again. A link inside the theme folder that points anywhere else is refused.
- Only image extensions are accepted: png, jpg, jpeg, gif, heic, heif, webp, tiff, tif, bmp.
- A single-file theme gets no files at all.
- Limits: 512 KiB per style sheet, 8 MiB per image, 4096 declarations, 200 characters per text token, 200 notes.
- A theme cannot make the shell unusable. Every number is clamped to the range in the table above, and every text colour is checked against the surface it sits on. A colour that would be unreadable is lightened or darkened until it reaches the required contrast (4.5:1 for body text, 3:1 for secondary text and glyphs), per appearance, with a note. Colours that are already readable are left exactly as they are.
Two themes to start from are in examples/themes:
minimal.css- a handful of colours in a single file.full/theme.css- every token with its default value, plus metadata and images. Copy it and change what you like.