Theme creation
Pearl themes come in two sizes. A local palette is one JSON file with as few as three colors: no package manifest, release version, stylesheet or source checkout is needed. A community package adds dark and light palettes, widget styling and publication metadata so other people can install it. Start with a palette; export it to a package when you want to share.
A palette supplies colors. Application profiles supply templates and activation instructions; importing colors does not import another shell or its profiles.
Local palette authoring uses either Settings → Appearance → Installed themes
& customization… or the installed
pearl-themes command, and both work on the same files under
$XDG_CONFIG_HOME/pearl/palettes/, normally ~/.config/pearl/palettes/.
Create a palette in Settings
Section titled “Create a palette in Settings”-
Open Palette and widget style and expand Create and edit palettes.
-
Set Filename to something like
meadowand Palette display name toMeadow. Filenames use lowercase letters, digits, dots, underscores and hyphens, start with a letter or digit, and keep the stem at most 80 characters. -
Enter your Background, Text and Accent as
#RRGGBBvalues, or keep the starter colors. The sample updates as you type; invalid colors or insufficient contrast produce a diagnostic and the sample keeps its previous valid appearance. -
Click Create. This writes a new file and refuses to replace an existing palette with that filename.
-
Find the palette in the installed list, click Use theme, then Apply & save.
Later edits use Edit colors on that palette. Save palette writes the file, and saving the currently selected palette updates Pearl and its enabled application themes automatically. Discard palette edits restores the last loaded source. These buttons act on the palette file directly, independently of the shared preferences draft. If the file changed externally since loading, saving reports a conflict: reopen Edit colors and reconcile first. Duplicate copies a palette under a new filename and Open file edits it in your own editor.
Create the same palette from a terminal
Section titled “Create the same palette from a terminal”pearl-themes init meadowpalette="${XDG_CONFIG_HOME:-$HOME/.config}/pearl/palettes/meadow.json"pearl-themes validate "$palette"pearl-themes preview "$palette" --watchSuccessful commands print JSON; validation errors print structured diagnostics
and exit nonzero. preview --watch opens a graphical preview that refreshes
after saved edits and needs a graphical session; creating, validating,
importing and exporting all work without a display. Matugen is not required
for a static shell palette.
The starter file is the entire authoring format:
{ "name": "Meadow", "dark": { "surface": "#101c19", "on_surface": "#e8f4e9", "primary": "#a4dfb0" }}name is optional; without it Pearl uses the filename stem. At least one
variant is required, and each supplied variant needs surface, on_surface
and primary, written as exactly six hexadecimal digits after #. JSON
comments, unknown keys and duplicate keys are rejected, and the source is
limited to 64 KiB. Dropping a valid JSON file into the palettes directory adds
it to the Settings list automatically.
With only a dark variant, both dark and light modes use that exact palette;
with only light, both use the light one. Pearl reports this fallback and
never manufactures an opposite variant, so author both when they should
differ:
{ "name": "Meadow", "dark": { "surface": "#101c19", "on_surface": "#e8f4e9", "primary": "#a4dfb0" }, "light": { "surface": "#f5fbf6", "on_surface": "#17231a", "primary": "#28633b" }}Keep a copy before replacing a palette. To undo a file edit, restore that copy. To change back, select Material or your previous palette and Apply & save.
Refine derived colors
Section titled “Refine derived colors”Every other color role is optional. Omitted roles are compiled from your three
colors: surface containers mix surface toward on_surface in steps,
secondary and tertiary default to primary, accent containers mix surface
toward their accent, and foregrounds on accents pick black or white by
contrast. Error and terminal colors fall back to Pearl’s built-in Material
defaults. Supply any role explicitly to keep its exact value:
{ "dark": { "surface": "#101c19", "on_surface": "#e8f4e9", "primary": "#a4dfb0", "secondary": "#91cef4", "hover": "#244136", "on_hover": "#e8f4e9" }}Expand Advanced colors and terminal palette · JSON in Settings to edit the full role set. Pearl checks a 4.5:1 contrast ratio for the main text, accent and error pairs and rejects unreadable explicit values instead of changing them; diagnostics name the JSON path and suggest a readable foreground.
Each variant can also carry an optional terminal object with background,
foreground, cursor, selection and ANSI normal/bright colors. Terminal
application templates (Ghostty, Kitty, Foot, Alacritty, WezTerm) consume these
values; missing slots use Material defaults derived from your palette.
Complete local palette reference
Section titled “Complete local palette reference”These case-sensitive names apply to compact local palettes, not the thirteen-role palettes inside full packages. See the palette parser and default color rules.
| Top-level key | Type and requirement |
|---|---|
name |
Optional text string, at most 256 bytes when nonempty; filename stem is the display fallback |
dark, light |
Color objects; at least one must be present and non-null. Each needs surface, on_surface, primary |
publication |
Optional metadata object for publishing; not needed for local use |
All roles are #RRGGBB strings without alpha. Only the first three are
required. “Mix X%” blends surface toward the named color; containers fall back
to surface if needed for contrast. “Readable foreground” chooses black or
white. Explicit values stay exact and must pass validation.
| Canonical role(s) in each variant | Default when omitted |
|---|---|
surface, on_surface, primary |
Required |
background, surface_dim, surface_container_lowest |
surface |
on_background |
on_surface |
surface_container_low |
Mix 4% toward on_surface |
surface_container |
Mix 8% toward on_surface |
surface_container_high |
Mix 12% toward on_surface |
surface_container_highest, surface_bright |
Mix 16% toward on_surface |
surface_variant |
Mix 8% toward on_surface |
on_surface_variant |
Mix 75% toward on_surface; use on_surface if needed for contrast |
outline, outline_variant |
Mix 55% and 25% toward on_surface, respectively |
secondary, tertiary |
primary |
on_primary, on_secondary, on_tertiary |
Readable foreground on the corresponding accent |
primary_container, secondary_container, tertiary_container |
Mix 20% toward the corresponding accent |
on_primary_container, on_secondary_container, on_tertiary_container |
on_surface if readable on that container, otherwise black or white |
primary_fixed, primary_fixed_dim |
primary |
secondary_fixed, secondary_fixed_dim |
secondary |
tertiary_fixed, tertiary_fixed_dim |
tertiary |
on_primary_fixed, on_primary_fixed_variant |
Readable foreground on primary |
on_secondary_fixed, on_secondary_fixed_variant |
Readable foreground on secondary |
on_tertiary_fixed, on_tertiary_fixed_variant |
Readable foreground on tertiary |
error, error_container |
Built-in Material defaults for that variant |
on_error, on_error_container |
Readable foreground on the respective error role |
shadow, scrim |
Built-in Material defaults for that variant |
surface_tint, source_color |
primary |
inverse_surface, inverse_on_surface |
on_surface, surface, respectively |
inverse_primary |
Readable foreground on on_surface |
hover, on_hover |
surface_container_high, on_surface, respectively |
Only these aliases are accepted. An alias and its canonical role may coexist if their colors match (case-insensitively); other camelCase names are not converted.
| Accepted alias | Canonical role |
|---|---|
mPrimary |
primary |
mOnPrimary |
on_primary |
mSecondary |
secondary |
mOnSecondary |
on_secondary |
mTertiary |
tertiary |
mOnTertiary |
on_tertiary |
mError |
error |
mOnError |
on_error |
mSurface |
surface |
mOnSurface |
on_surface |
mSurfaceVariant |
surface_variant |
mOnSurfaceVariant |
on_surface_variant |
mOutline |
outline |
mShadow |
shadow |
mHover |
hover |
mOnHover |
on_hover |
Terminal fields
Section titled “Terminal fields”Each variant accepts an optional terminal object. Its color values are
optional #RRGGBB strings; use the exact names below.
Field below terminal |
Default |
|---|---|
background, foreground |
surface, on_surface |
cursor, cursorText |
primary, on_primary |
selectionBg, selectionFg |
primary_container, on_primary_container |
normal, bright |
Objects with any of black, red, green, yellow, blue, magenta, cyan, white |
ANSI normal black uses surface; white in both groups uses on_surface.
Bright black and the six colored slots use Material base16 defaults for the
variant unless overridden. Missing slots and transport tone ramps are not
generated from your accent.
Rejected input and contrast checks
Section titled “Rejected input and contrast checks”onPrimary, surfaceContainer, mPrimaryContainer, CSS color names, #RGB,
#RRGGBBAA, arrays of ANSI colors, foreign template settings and arbitrary
metadata keys are rejected. Use the canonical names above. Files must be strict
JSON, at most 64 KiB, without duplicate keys or excessive nesting.
The compiler checks 4.5:1 for these foreground/background pairs:
on_surface/surface, on_surface/surface_container_high,
on_surface_variant/surface_container, each accent’s on_…/accent and
on_…_container/container, error/error_container, and on_hover/hover.
ANSI pairs are not checked; inspect the terminal colors too.
Theme applications with the same palette
Section titled “Theme applications with the same palette”Under Appearance → Application themes, enable management, choose Use Material defaults and Apply & save, and keep application colors on Follow Pearl. Local palettes supply complete render input to the built-in application templates, so you do not need a copy of each template in your palette. Matugen 4.x is required for application rendering. See Appearance for activation and recovery details.
Import Noctalia colors
Section titled “Import Noctalia colors”Wrap imported colors in dark or light using the roles or aliases above.
For example, save this as noctalia-meadow.json:
{ "name": "Noctalia Meadow", "dark": { "mSurface": "#101c19", "mOnSurface": "#e8f4e9", "mPrimary": "#a4dfb0", "mSecondary": "#abcdef" }}Import converts these aliases to surface, on_surface, primary and
secondary. The last is an accent; use on_surface_variant for muted text.
Map other exports to these roles and leave out their template or shell settings.
pearl-themes validate ./noctalia-meadow.jsonpearl-themes import ./noctalia-meadow.json --name noctalia-meadowpalette="${XDG_CONFIG_HOME:-$HOME/.config}/pearl/palettes/noctalia-meadow.json"pearl-themes validate "$palette"Successful commands return "valid": true; import also reports
local.palette.noctalia-meadow and its saved path. It refuses to overwrite an
existing file. To activate it, open Appearance → Installed themes &
customization… → Palette and widget style, choose Use theme, then Apply
& save. A dark-only palette is used in both variants.
To import in Settings, expand Import and export, enter the absolute input
path and a new Filename, then click Import palette.
ConflictingPaletteAlias means an alias and its canonical role disagree;
remove one or make their values match.
DMS Matugen templates need a Pearl profile manifest and adapter; this command imports colors only.
Export a community package
Section titled “Export a community package”When the palette is ready to distribute, write a metadata.json with a stable
identity, version and your actual licensing text:
{ "id": "org.example.meadow", "author": "Your name", "license": "MIT", "source": "https://github.com/your-name/pearl-palettes", "version": "1.0.0", "license_text": "The complete license and copyright notice for your palette.", "attribution": "Describe the original palette and credit borrowed colors or assets."}Use your own ID outside the reserved pearl.* and local.palette.*
namespaces, three numeric version components, and replace every placeholder:
license and attribution text are required for publication. Then:
pearl-themes export "$palette" --output ./meadow-package --metadata ./metadata.jsonpearl-themes validate ./meadow-packagepearl-themes '{"action":"pack","path":"./meadow-package","output":"./meadow-1.0.0.tar.gz"}'Export writes a validated package directory containing the palettes, render
data, LICENSE and ATTRIBUTION.md; pack turns it into an archive and prints
its SHA-256 and size. Test the result with
Community repositories and local archives in Settings, or:
pearl-themes '{"action":"import_archive","path":"./meadow-1.0.0.tar.gz"}'Importing installs the package without changing the active appearance. In Settings under Appearance → Community themes, choose Installed, then Preview, Use theme and Apply & save. Installed releases are separate from your editable local palette and keep their committed appearance until you adopt a new version.
Publish a palette in a GitHub repository
Section titled “Publish a palette in a GitHub repository”A public GitHub repository can host compact palette sources directly:
palettes/ meadow.jsonPut the metadata object from the export step under a top-level publication
key in the palette file, beside name, dark and light. Only flat
palettes/*.json entries are discovered. Commit the file, add the repository
to Pearl’s community browser (below), and refresh.
Pearl pins published sources to a Git commit and blob hash, so changing source
bytes under the same ID and version is rejected as MutableThemeRelease:
increment publication.version whenever you change a published palette.
Community repositories
Section titled “Community repositories”Pearl includes the
Seafoam Labs community repository
by default. Any other public GitHub repository can be added by its
https://github.com/OWNER/REPO URL under Appearance → Community repositories
and local archives. On Community · refresh, Pearl resolves the default
branch to a commit and discovers themes/*/theme.json packages and
palettes/*.json compact sources, sixteen entries per page. Installation
fetches raw files at that pinned commit and verifies every file against its
Git blob hash and size before the normal validation and atomic install.
Metadata is cached for offline browsing, but installing still requires the
files to be reachable, and GitHub’s public API rate limits apply.
Full packages committed one directory below themes/ need no index, archive
upload or GitHub release. To contribute to the built-in repository, open a pull
request adding your theme folder there, following that repository’s
contribution instructions; the theme becomes discoverable once it reaches the
default branch.
Custom HTTPS index repositories
Section titled “Custom HTTPS index repositories”Independently hosted catalogs remain supported: an HTTPS index.json listing
immutable tar.gz archives with exact SHA-256 hashes and byte sizes, paged in
groups of at most sixteen releases through next URLs. Validate a page before
publishing:
pearl-themes '{"action":"validate_index","path":"index.json","id":"my-community"}'Upload archives first, then publish the index, and never change the bytes of
an existing (repository URL, package ID, version) triple: publish a new
version instead. Sources are configured in Settings and stored in
$XDG_CONFIG_HOME/pearl/theme-repositories.json, with at most sixteen
sources. The full index schema, publication workflow and limits are documented
in the Pearl
repository.
Richer packages: widget styling and images
Section titled “Richer packages: widget styling and images”For custom widget geometry, component CSS, images or bundled application
profiles, author a full package instead of a compact palette. Its manifest
(theme.json) points at dark and light palettes of thirteen colors each,
optional shape and motion tokens, and an optional small component stylesheet:
{ "schema_version": 1, "id": "org.example.my-theme", "name": "My Theme", "author": "Your name", "license": "CC0-1.0", "source": "https://github.com/YOUR-NAME/pearl-themes", "asset_version": "1.0.0", "requires": { "palette_api": 1, "style_api": 1 }, "palettes": { "dark": "dark.json", "light": "light.json" }, "style": { "tokens": "tokens.json", "css": "components.css" }}The workflow mirrors palette publishing: start from the included example in
the Pearl source tree, validate and pack with pearl-themes, import the
archive locally to preview, then commit the folder under themes/ in a public
GitHub repository or ship archives through a custom index. Increase
asset_version for every published change. The step-by-step guide, the
supported CSS grammar, token ranges and error reference live in the Pearl
repository.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Next action |
|---|---|
| Palette absent from the list | Check the directory, filename and .json extension, then run pearl-themes validate FILE |
InvalidColor or MissingPaletteRole |
Supply each required role as #RRGGBB in every authored variant |
InsufficientContrast |
Fix the reported pair; the diagnostic suggests a readable foreground |
Conflict while saving |
Reopen Edit colors and reconcile the external change |
PaletteAlreadyExists |
Choose a new filename or edit the existing palette |
| Edits have no effect | Select the palette and Apply & save, then Save palette or save the file |
| Both modes look identical | Author both variants instead of relying on single-variant fallback |
| Application colors do not follow | Enable management, choose defaults or a profile, select Follow Pearl, and read the row’s status |
MutableThemeRelease |
Increment the published version for changed source |
