Skip to content

Latest commit

 

History

History
296 lines (216 loc) · 7.22 KB

custom-css.md

File metadata and controls

296 lines (216 loc) · 7.22 KB

Custom CSS and embedded SCSS

Documentation · Previous: Style variations · Next: SCSS export

The examples below run inside an entrypoint callable with a ThemeJson parameter. Embedded SCSS is compiled into the JSON styles.css field. To create an SCSS partial for a separate Sass build, use SCSS reference export.

WordPress handles global and scoped custom CSS differently. The generator keeps those two formats separate and validates scoped CSS before writing it to theme.json.

Use styles()->css() and styles()->appendCss() for global CSS, or call the same methods after blocks() or elements() for scoped CSS. The first argument contains the CSS. The optional second argument declares its root selector, which the generator removes and converts into WordPress & scoped syntax.

Choose the CSS format

css(), appendCss(), scss(), and appendScss() accept an optional selector as their second argument:

$styles->css($css, $selector);

Without it, the generator expects CSS already written for its destination:

  • global CSS uses complete rules;
  • scoped CSS uses declarations and nested rules beginning with &.

With it, the generator accepts regular CSS rooted at that selector and converts it to the scoped format required by WordPress:

$button = $themeJson->styles()->blocks('core/button');
$button->css(
    '.wp-block-button { color: red; }'
    . '.wp-block-button a { color: blue; }',
    '.wp-block-button'
);

This produces a scoped value equivalent to:

color: red;
& a {
    color: blue;
}

Every input selector must be the supplied selector or structurally belong to it. Unrelated selectors cause an InvalidArgumentException.

Global CSS

Global CSS is stored in styles.css. WordPress adds it to the stylesheet without changing it, so write complete CSS rules:

$themeJson
    ->styles()
    ->css('body { color: red; }');

Use appendCss() to add more global rules without replacing existing CSS:

$styles = $themeJson->styles();

$styles->css('body { color: red; }');
$styles->appendCss('a { color: blue; }');

The resulting theme.json contains:

{
    "styles": {
        "css": "body { color: red; }a { color: blue; }"
    }
}

Global CSS is the correct place for independent selectors, at-rules, and rules that are not owned by a block or element:

$themeJson->styles()->css(
    '@media (min-width: 48rem) { .card_title { display: grid; } }'
);

Scoped CSS

CSS configured through blocks() or elements() is scoped by WordPress:

$button = $themeJson
    ->styles()
    ->blocks('core/button');

$button->css('color: red;');
$button->appendCss('& a { color: blue; }');

Scoped CSS uses the format expected by WP_Theme_JSON:

  • declarations without braces apply to the current block or element;
  • each nested rule starts with &;
  • WordPress replaces & with its resolved selector.

The stored value is:

color: red;& a { color: blue; }

WordPress then generates selectors for the configured block. The exact root selector comes from WordPress block metadata.

Accepted syntax

Apply declarations to the current scope:

$button->css('color: red; padding: 1rem;');

Target a descendant:

$button->appendCss('& .icon { color: blue; }');

Target a state:

$button->appendCss('&:hover { color: blue; }');

Target a child or sibling:

$button->appendCss('& > span { display: block; }');
$button->appendCss('& + .notice { margin-top: 1rem; }');

Rejected syntax

A nested scoped rule without & is invalid:

// Wrong: WordPress would concatenate this with the root declarations.
$button->appendCss('a { color: blue; }');

The generator throws instead of allowing WordPress to produce malformed CSS. Write this:

$button->appendCss('& a { color: blue; }');

An independent selector does not belong inside scoped CSS:

$card = $themeJson->styles()->blocks('core/group');
// Wrong: .card_title is not inside the .card scope.
$card->css('.card { color: red; } .card_title { color: blue; }', '.card');

Choose the intended relationship explicitly:

// Descendant: .card .card_title
$card->css('.card { color: red; } .card .card_title { color: blue; }', '.card');

// Independent selector: write it as global CSS.
$themeJson->styles()->css('.card_title { color: blue; }');

The same rule applies to hyphenated names:

// Wrong: .card-title is a separate class, not a descendant of .card.
$card->css('.card-title { color: blue; }', '.card');

Sharing a string prefix does not make selectors part of the same scope.

Selector Scope

When using the selector argument, every selector in the input must be the scope itself or structurally belong to it:

/* Accepted */
.card
.card:hover
.card .title
.card > .title
.card + .notice

/* Rejected */
.other
.card-title
.card_title

Rejected selectors cause an InvalidArgumentException. The message identifies the selector and recommends either a descendant selector or global CSS. Nothing is silently discarded.

appendCss()

css() writes the scoped CSS value. appendCss() preserves that value and adds the new declarations or nested rules:

$button->css('color: red;');
$button->appendCss('&:hover { color: blue; }');

Use the same syntax consistently:

  • without the selector argument, provide WordPress scoped syntax yourself;
  • with the selector argument, provide regular CSS rooted at that selector.
$button->css('.button { color: red; }', '.button');
$button->appendCss('.button:hover { color: blue; }', '.button');

SCSS

scss() and appendScss() compile SCSS first, then apply the same scope validation as CSS:

$button->scss(
    '.button {'
    . '  color: red;'
    . '  &:hover { color: blue; }'
    . '}',
    '.button'
);

Compiled selectors must still belong to the requested scope. For example, SCSS &__title compiles to the independent class .card__title; it is not a descendant of .card and is rejected in .card-scoped CSS.

Use an actual descendant when that is the intended result:

.card {
    .card__title {
        color: blue;
    }
}

Use global scss() when the compiled stylesheet contains independent selectors.

At-rules

WordPress does not preserve @media or @supports correctly inside scoped block custom CSS. The generator rejects scoped at-rules:

// Wrong: rejected in a block or element scope.
$button->css('@media (min-width: 48rem) { color: blue; }');

Place the complete rule in global CSS instead:

$themeJson->styles()->css(
    '@media (min-width: 48rem) { .wp-block-button { color: blue; } }'
);

Choose the matching API

Goal API and syntax
Complete independent rules Global styles()->css()
Add another independent rule Global styles()->appendCss()
Style the current block or element Scoped css('color: red;')
Style a descendant or state Scoped appendCss('& ... { ... }')
Convert regular scoped CSS Pass its root selector as the second argument
Add scoped at-rules Not supported; use global CSS
Use independent selectors in scoped CSS Not supported; use global CSS