Skip to content

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

  1. Open Palette and widget style and expand Create and edit palettes.

  2. Set Filename to something like meadow and Palette display name to Meadow. Filenames use lowercase letters, digits, dots, underscores and hyphens, start with a letter or digit, and keep the stem at most 80 characters.

  3. Enter your Background, Text and Accent as #RRGGBB values, 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.

  4. Click Create. This writes a new file and refuses to replace an existing palette with that filename.

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

Terminal window
pearl-themes init meadow
palette="${XDG_CONFIG_HOME:-$HOME/.config}/pearl/palettes/meadow.json"
pearl-themes validate "$palette"
pearl-themes preview "$palette" --watch

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

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.

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

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.

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.

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.

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.

Terminal window
pearl-themes validate ./noctalia-meadow.json
pearl-themes import ./noctalia-meadow.json --name noctalia-meadow
palette="${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.

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:

Terminal window
pearl-themes export "$palette" --output ./meadow-package --metadata ./metadata.json
pearl-themes validate ./meadow-package
pearl-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:

Terminal window
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.

A public GitHub repository can host compact palette sources directly:

palettes/
meadow.json

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

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.

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:

Terminal window
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.

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
© 2026 Seafoam LabsShelly Chel