Skip to content
MCG Filament CMS Demo

Building a skin

This tutorial builds one skin, Aurora, from the Artisan generator to a skin an administrator can choose in the admin panel. A skin is a stylesheet that reassigns the package's design tokens: the CSS custom properties that hold every color, typeface and measurement the package's templates use. A skin changes no template. The tutorial is written for a developer adding a visual theme to a site that runs the mcg/filament-cms package.

The package ships six skins: press, signal, marquee, saas, arcade and market, in resources/css/skins/. They use the same mechanism as a skin written for one site: a stylesheet scoped to the skin's name, and a registered name that makes the package write data-cms-skin="{name}" onto <html>.

1. Generate the stylesheet

  1. In a terminal at the site's root, run the generator with the skin's name:

Shell

php artisan cms:make-skin Aurora

The command prints Skin [aurora] created., the path of the file it wrote, the two remaining steps (steps 2 and 3 below), and a warning about fonts.

The command writes resources/css/skins/aurora.css. The file contains a [data-cms-skin='aurora'] block of placeholder tokens to replace, the two dark-theme declarations described in step 5, and a @layer components block for rules that a token cannot express. The generator converts the name with Str::slug(), so php artisan cms:make-skin "Deep Aurora" produces the skin key deep-aurora.

The --partial option also writes resources/views/cms/skins/aurora.blade.php. <x-cms::head> includes that partial only while the skin is active. A partial is needed only for values CSS cannot read, such as the scroll position, which the partial publishes as custom properties for the stylesheet to use. A skin made of tokens alone needs no partial.

2. Register the skin's name

The generator prints this step instead of performing it, because config/cms.php may be a published copy, the package's untouched default, or a file with uncommitted changes.

  1. In config/cms.php, add the skin's key and label to theme.skins:

config/cms.php PHP

'theme' => [
    'skins' => ['aurora' => 'Aurora'],
],

A skin distributed as its own Composer package registers itself from its service provider instead, so a site installing it does not edit its config:

src/AuroraSkinServiceProvider.php PHP

public function boot(): void
{
    app(SkinRegistry::class)->register('aurora', 'Aurora');

    // The stylesheet, published however the package ships it.
    $this->publishes([
        __DIR__.'/../resources/css/aurora.css' => resource_path('css/skins/aurora.css'),
    ], 'aurora-skin');
}

After either registration, the name is valid: it is offered in the Skin field on the Site settings screen, and choosing it writes data-cms-skin="aurora" onto <html>. Setting cms.theme.skin to 'aurora' in config/cms.php selects it as well.

3. Import the stylesheet

The generator also prints this step instead of performing it.

  1. In resources/css/app.css, import the skin's stylesheet after the line that imports the package's cms.css:

resources/css/app.css CSS

@import 'tailwindcss' source(none);
@import '../../vendor/mcg/filament-cms/resources/css/cms.css';
@import './skins/aurora.css';  /* the skin, last */

The skin's declarations and the package's declarations have the same specificity, so the one imported later takes effect. Until steps 2 and 3 are both done, the skin's stylesheet has no effect: nothing imports it, or the attribute it is scoped to is never written onto <html>.

4. Replace the tokens

The generator's palette is a neutral placeholder. Replacing it is the whole of a skin; no template is modified.

  1. In resources/css/skins/aurora.css, replace the [data-cms-skin='aurora'] block with the skin's values:

resources/css/skins/aurora.css CSS

[data-cms-skin='aurora'] {
    /* ---- Type ---- */
    --font-cms-display: 'Fraunces', Georgia, serif;
    --font-cms-body: 'Inter', system-ui, sans-serif;
    --font-cms-prose: 'Newsreader', Georgia, serif;

    /* ---- Surfaces ---- */
    --color-cms-surface: oklch(0.99 0.005 90);
    --color-cms-raised: #fff;
    --color-cms-sunken: oklch(0.96 0.01 90);

    /* ---- Text ---- */
    --color-cms-ink: oklch(0.22 0.02 60);
    --color-cms-muted: oklch(0.48 0.02 60);
    --color-cms-faint: oklch(0.53 0.02 60);

    /* ---- Lines ---- */
    --color-cms-line: oklch(0.9 0.01 60);
    --color-cms-line-strong: oklch(0.82 0.02 60);

    /* ---- Accent: four tokens, changed together ---- */
    --color-cms-accent: oklch(0.52 0.16 25);
    --color-cms-accent-hover: oklch(0.45 0.17 25);
    --color-cms-accent-ink: oklch(0.99 0 0);
    --color-cms-accent-faint: oklch(0.95 0.03 25);

    /* ---- Shape and rhythm ---- */
    --radius-cms: 0;
    --radius-cms-sm: 0;
    --radius-cms-lg: 0;

    /* Three of the four width presets a block can choose. The fourth,
       full bleed, has no maximum width to set. */
    --cms-width-narrow: 42rem;
    --cms-width-normal: 56rem;
    --cms-width-wide: 80rem;

    --cms-measure: 68ch;
    --cms-gutter: 1.5rem;
}

5. Declare the dark theme

The dark theme is declared twice, and the generator writes both declarations. The first applies the operating system's dark preference unless the visitor chose light with the header toggle. The second applies when the visitor chose dark with the header toggle, which writes data-cms-theme="dark" onto <html>, including on a machine that prefers light. A media query alone cannot let the visitor's choice override the operating system in both directions. Components need no changes, because they refer only to tokens.

  1. In resources/css/skins/aurora.css, replace the two dark-theme declarations with the skin's dark values, the same set in both:

resources/css/skins/aurora.css CSS

/* The operating system preference, unless the visitor chose light. */
@media (prefers-color-scheme: dark) {
    :root[data-cms-skin='aurora']:not([data-cms-theme='light']) {
        --color-cms-surface: oklch(0.16 0.01 60);
        --color-cms-raised: oklch(0.2 0.012 60);
        --color-cms-sunken: oklch(0.13 0.01 60);

        --color-cms-ink: oklch(0.95 0.01 60);
        --color-cms-muted: oklch(0.72 0.015 60);
        --color-cms-faint: oklch(0.61 0.015 60);

        --color-cms-line: oklch(0.28 0.012 60);
        --color-cms-line-strong: oklch(0.36 0.015 60);

        --color-cms-accent: oklch(0.7 0.14 25);
        --color-cms-accent-ink: oklch(0.15 0.01 60);
        --color-cms-accent-faint: oklch(0.26 0.04 25);
    }
}

/* The header toggle, which also applies on a machine that prefers light. */
:root[data-cms-skin='aurora'][data-cms-theme='dark'] {
    --color-cms-surface: oklch(0.16 0.01 60);
    --color-cms-raised: oklch(0.2 0.012 60);
    --color-cms-sunken: oklch(0.13 0.01 60);

    --color-cms-ink: oklch(0.95 0.01 60);
    --color-cms-muted: oklch(0.72 0.015 60);
    --color-cms-faint: oklch(0.61 0.015 60);

    --color-cms-line: oklch(0.28 0.012 60);
    --color-cms-line-strong: oklch(0.36 0.015 60);

    /* A lighter accent: a color tuned for a white background is usually too
       dark to read on a near-black one. */
    --color-cms-accent: oklch(0.7 0.14 25);
    --color-cms-accent-ink: oklch(0.15 0.01 60);
    --color-cms-accent-faint: oklch(0.26 0.04 25);
}

Declare the same tokens in both blocks. A token set only in the second block keeps its light value for a visitor whose operating system prefers dark, for example a white --color-cms-raised card on a near-black page.

6. Restyle a component

Tokens cover most visual changes. When a component needs a different shape, override its semantic class. Every component class is named cms-*, and no markup has to change.

  1. In resources/css/skins/aurora.css, add the overrides inside the existing @layer components block:

resources/css/skins/aurora.css CSS

@layer components {
    /* Square, underlined and unfilled: a press-style button. */
    .cms-button--primary {
        @apply px-0 py-1;
        background: transparent;
        color: var(--color-cms-ink);
        border-bottom: 2px solid var(--color-cms-accent);
        border-radius: 0;
    }

    .cms-block__heading {
        letter-spacing: -0.03em;
    }
}
  1. In the same terminal as step 1, rebuild the assets:

Shell

npm run build

The package's own component rules are in @layer components, and the skin's overrides go there too. Tailwind utilities sit in a later layer, so a utility class written directly in a Blade view still overrides both.

Common mistakes