This guide covers the baseStyles section of c2b.config.json. Base styles define root-level typography, colors, spacing, and element defaults that are generated as both SCSS (for Storybook) and theme.json styles (for WordPress).
The baseStyles section sits at the top level of the config alongside prefix, output, and tokens. It references tokens by key — the generator resolves them to the correct CSS variable format for each output target.
{
"prefix": "mylib",
"output": { "...": "..." },
"tokens": {
"color": { "...": "..." },
"fontFamily": { "...": "..." },
"spacing": { "...": "..." }
},
"baseStyles": {
"body": {
"fontFamily": "inter",
"fontSize": "medium",
"fontWeight": "normal",
"lineHeight": "normal",
"color": "black",
"background": "grey-faint"
},
"heading": {
"fontFamily": "inter",
"color": "primary"
},
"h1": { "fontSize": "5x-large", "fontWeight": "medium" },
"h2": { "fontSize": "4x-large", "fontWeight": "medium" },
"h3": { "fontSize": "3x-large", "fontWeight": "medium" },
"h4": { "fontSize": "2x-large", "fontWeight": "medium" },
"h5": { "fontSize": "x-large", "fontWeight": "medium" },
"h6": { "fontSize": "large", "fontWeight": "medium", "fontStyle": "italic" },
"caption": { "fontSize": "small", "fontStyle": "italic", "fontWeight": "light", "color": "warning" },
"button": { "color": "white", "background": "primary" },
"link": { "color": "primary", "hoverColor": "error" },
"spacing": {
"blockGap": "medium",
"padding": {
"top": "0",
"right": "large",
"bottom": "0",
"left": "large"
}
}
}
}Every string in baseStyles is classified at config load time as either a token reference (matches a key in the property's expected category), a raw CSS value (numeric, hex, function, multi-value, quoted, or a known CSS keyword for the property), or invalid (typo or dangling reference — throws a clear error before any files are written). See Strict Validation below for details.
Sets the root-level typography and color for the page. Maps to styles.typography and styles.color in theme.json.
{
"body": {
"fontFamily": "inter",
"fontSize": "medium",
"fontWeight": "normal",
"lineHeight": "normal",
"color": "black",
"background": "grey-faint"
}
}SCSS output:
body {
font-family: var(--mylib--font-family-inter);
font-size: var(--mylib--font-size-medium);
font-weight: var(--mylib--font-weight-normal);
line-height: var(--mylib--line-height-normal);
color: var(--mylib--color-black);
background-color: var(--mylib--color-grey-faint);
}theme.json output:
{
"styles": {
"typography": {
"fontFamily": "var(--wp--preset--font-family--inter)",
"fontSize": "var(--wp--preset--font-size--medium)",
"fontWeight": "400",
"lineHeight": "1.6"
},
"color": {
"text": "var(--wp--preset--color--black)",
"background": "var(--wp--preset--color--grey-faint)"
}
}
}Note:
fontWeightandlineHeightbelong to custom-only categories that don't produce--wp--preset--*variables. In theme.jsonstyles, the generator emits the token's underlying value (e.g."400","1.6") so WordPress receives valid CSS. In SCSS, the same token resolves to avar(--mylib--font-weight-normal)reference.
Applies shared styles to all heading levels (h1–h6). In SCSS, this generates a :where(h1, h2, h3, h4, h5, h6) rule. In theme.json, it maps to styles.elements.heading.
{
"heading": {
"fontFamily": "inter",
"color": "primary"
}
}SCSS output:
:where(h1, h2, h3, h4, h5, h6) {
font-family: var(--mylib--font-family-inter);
color: var(--mylib--color-primary);
}Individual heading levels override or extend the shared heading styles. Each generates a :where(hN) rule in SCSS and an styles.elements.hN entry in theme.json.
You can reference size tokens (recommended — so headings participate in your fluid type scale) or use raw values:
{
"h1": { "fontSize": "5x-large", "fontWeight": "medium" },
"h2": { "fontSize": "4x-large", "fontWeight": "medium" },
"h6": { "fontSize": "large", "fontWeight": "medium", "fontStyle": "italic" }
}SCSS output:
:where(h1) {
font-size: var(--mylib--font-size-5x-large);
font-style: normal;
font-weight: var(--mylib--font-weight-medium);
}
:where(h6) {
font-size: var(--mylib--font-size-large);
font-style: italic;
font-weight: var(--mylib--font-weight-medium);
}Note: Individual headings (h1–h6) automatically default to
fontStyle: normalwhen nofontStyleis specified. This prevents headings from inheriting an italic style from the sharedheadingelement or the browser's defaults. To make a heading italic, set"fontStyle": "italic"explicitly (as shown with h6 above).The
fontStyle: "normal"default correctly emits a bare CSS keyword in the generated SCSS. Strict per-property resolution means it cannot accidentally cross-resolve to afontWeight.normaltoken even when one exists.
Tip — keep heading sizes out of the Gutenberg size picker. If your heading sizes are meant for typography base styles only and shouldn't appear in the block editor's font-size dropdown, mark them cssOnly: true in tokens.fontSize. They still get emitted as CSS variables and can still be referenced from baseStyles, but they won't clutter the picker with "display" sizes content authors don't need.
Styles <figcaption> elements. Generates a :where(figcaption) rule in SCSS and styles.elements.caption in theme.json.
{
"caption": {
"fontSize": "small",
"fontStyle": "italic",
"fontWeight": "light",
"color": "warning"
}
}SCSS output:
:where(figcaption) {
font-size: var(--mylib--font-size-small);
font-style: italic;
font-weight: var(--mylib--font-weight-light);
color: var(--mylib--color-warning);
}Styles <button> elements. Generates a :where(button) rule in SCSS and styles.elements.button in theme.json.
{
"button": {
"color": "white",
"background": "primary"
}
}SCSS output:
:where(button) {
color: var(--mylib--color-white);
background-color: var(--mylib--color-primary);
}Styles <a> elements. The hoverColor property generates a separate :where(a:hover) rule in SCSS and a :hover pseudo-class in theme.json.
{
"link": {
"color": "primary",
"hoverColor": "error"
}
}SCSS output:
:where(a) {
color: var(--mylib--color-primary);
}
:where(a:hover) {
color: var(--mylib--color-error);
}theme.json output:
{
"styles": {
"elements": {
"link": {
"color": { "text": "var(--wp--preset--color--primary)" },
":hover": {
"color": { "text": "var(--wp--preset--color--error)" }
}
}
}
}
}Note:
hoverColoris exclusive to thelinkelement. It's the only element that supports a hover pseudo-class.
The spacing key within baseStyles controls root-level block gap and page padding. It's separate from the spacing token category — tokens define the scale, while baseStyles.spacing applies those tokens to the page layout.
{
"baseStyles": {
"spacing": {
"blockGap": "medium",
"padding": {
"top": "0",
"right": "large",
"bottom": "0",
"left": "large"
}
}
}
}Controls the default vertical spacing between WordPress blocks. Generates a CSS custom property and layout utility rules:
SCSS output:
body {
--mylib--root-block-gap: var(--mylib--spacing-medium);
}
:where(.is-layout-constrained) > * + * {
margin-block-start: var(--mylib--root-block-gap);
}
:where(.is-layout-flex) {
gap: var(--mylib--root-block-gap);
}
:where(.is-layout-grid) {
gap: var(--mylib--root-block-gap);
}The layout utility rules mirror WordPress's block gap behavior:
.is-layout-constrained— Flow layout: appliesmargin-block-startbetween consecutive children.is-layout-flex— Flex layout: appliesgapon the container.is-layout-grid— Grid layout: appliesgapon the container
theme.json output:
{
"styles": {
"spacing": {
"blockGap": "var(--wp--preset--spacing--50)"
}
}
}Controls root-level page padding. Only defined sides are output. Generates root padding CSS custom properties and WordPress-compatible global padding utility classes:
SCSS output:
body {
--mylib--root-padding-top: 0;
--mylib--root-padding-right: var(--mylib--spacing-large);
--mylib--root-padding-bottom: 0;
--mylib--root-padding-left: var(--mylib--spacing-large);
}
.has-global-padding {
padding-right: var(--mylib--root-padding-right);
padding-left: var(--mylib--root-padding-left);
}
.has-global-padding > .alignfull {
max-width: none;
margin-right: calc(var(--mylib--root-padding-right) * -1);
margin-left: calc(var(--mylib--root-padding-left) * -1);
}
.has-global-padding > .alignfull > .has-global-padding {
padding-right: var(--mylib--root-padding-right);
padding-left: var(--mylib--root-padding-left);
}The .has-global-padding and .alignfull rules mirror WordPress's root padding-aware alignment system. Full-width blocks break out of the content padding, while nested content within them retains it.
theme.json output:
{
"styles": {
"spacing": {
"padding": {
"top": "0",
"right": "var(--wp--preset--spacing--60)",
"bottom": "0",
"left": "var(--wp--preset--spacing--60)"
}
}
}
}See Spacing for the full spacing token configuration reference.
All elements support the following properties. Values are classified strictly per property: they either resolve to a token in the property's expected category, pass through as a raw CSS value, or fail validation.
| Property | Expected token category | CSS keyword fallbacks |
|---|---|---|
fontFamily |
fontFamily |
serif, sans-serif, monospace, cursive, fantasy, system-ui, ui-serif, ui-sans-serif, ui-monospace, inherit, initial, unset |
fontSize |
fontSize |
— (use tokens or raw values) |
fontWeight |
fontWeight |
normal, bold, lighter, bolder, inherit, initial, unset |
lineHeight |
lineHeight |
normal, inherit, initial, unset |
fontStyle |
— (no category) | normal, italic, oblique, inherit, initial, unset |
| Property | Expected token category | CSS keyword fallbacks |
|---|---|---|
color |
color |
inherit, transparent, currentColor, initial, unset |
background |
color |
inherit, transparent, currentColor, initial, unset |
hoverColor |
color (link only) |
inherit, transparent, currentColor, initial, unset |
| Property | Expected token category | CSS keyword fallbacks |
|---|---|---|
spacing.padding.{top,right,bottom,left} |
spacing |
— |
spacing.blockGap |
spacing |
— |
Each string in baseStyles goes through one classification step, in this order:
- Token lookup in the property's category. If the value is a key in the expected category (e.g.
fontSize: "medium"andtokens.fontSize.mediumexists), it resolves to a CSS variable. Lookup is strict — there is no cross-category fallback, sofontSize: "large"will not match aspacing.largetoken. - Raw CSS detection. If the value starts with a digit/dot/minus, starts with
#, contains a function call ((), contains whitespace or a comma, or is quoted, it passes through as-is. - CSS keyword whitelist. If the value matches a known CSS keyword for the property (see the tables above), it passes through as-is. A token with the same key always wins over the keyword.
- Invalid. Anything else is a typo or a stale token reference. See Strict Validation.
Examples assuming tokens.fontFamily.inter, tokens.fontSize.medium, tokens.color.primary, tokens.fontWeight.medium, and tokens.lineHeight.normal all exist:
| Config value | For property | Classification | SCSS output | theme.json output |
|---|---|---|---|---|
"inter" |
fontFamily |
token | var(--mylib--font-family-inter) |
var(--wp--preset--font-family--inter) |
"medium" |
fontSize |
token | var(--mylib--font-size-medium) |
var(--wp--preset--font-size--medium) |
"primary" |
color |
token | var(--mylib--color-primary) |
var(--wp--preset--color--primary) |
"medium" |
fontWeight |
token (custom-only category) | var(--mylib--font-weight-medium) |
underlying value (e.g. "500") |
"normal" |
lineHeight |
token (custom-only category) | var(--mylib--line-height-normal) |
underlying value (e.g. "1.6") |
"italic" |
fontStyle |
raw (CSS keyword) | italic |
"italic" |
"normal" |
fontStyle |
raw (CSS keyword) | normal |
"normal" |
"sans-serif" |
fontFamily |
raw (CSS keyword) | sans-serif |
"sans-serif" |
"4.5rem" |
fontSize |
raw (numeric) | 4.5rem |
"4.5rem" |
"500" |
fontWeight |
raw (numeric) | 500 |
"500" |
"1.6" |
lineHeight |
raw (numeric) | 1.6 |
"1.6" |
"#191919" |
color |
raw (hex) | #191919 |
"#191919" |
"var(--accent)" |
color |
raw (function) | var(--accent) |
"var(--accent)" |
Why custom-only tokens resolve to their underlying value in theme.json: categories like
fontWeight,lineHeight, andradiusdo not have--wp--preset--*mappings. Emitting a semantic slug like"medium"into theme.jsonstyleswould give WordPress invalid CSS (font-weight: medium). The generator looks up the token and emits its raw value instead (e.g."500"), so WordPress receives valid CSS while SCSS consumers still get a themeable variable reference.Why
cssOnlytokens resolve to their underlying value in theme.json:cssOnlypreset tokens are excluded fromsettings.*.*in theme.json, which means the corresponding--wp--preset--*variable will not exist in WordPress. The generator falls back to the raw value so the resulting CSS still works. SCSS output is unaffected — it still emitsvar(--prefix--{segment}-{key})becausetokens.cssdefines that variable.
baseStyles values are validated at config load time, before any files are written. Every string is classified using the rules above. Any value that doesn't fall into token, raw, or CSS keyword throws an error with full context:
Config error: baseStyles.body.color = "text-black" is not a valid token or CSS keyword for "color".
Expected a token key from tokens.color (available: primary, primary-dark, black, grey-dark, grey, grey-light, grey-faint, white).
Or use one of these CSS keywords: inherit, transparent, currentColor, initial, unset.
Or provide a raw CSS value (numeric, hex, rgb(), var(), calc(), multi-value, or quoted string).
Common cases strict validation catches:
- Stale references. You remove
tokens.color.text-blackbut forget to updatebaseStyles.body.color. Previously the value would pass through as a literal (color: text-black) and silently break the page. Strict validation fails the build instead. - Cross-category typos. You type
fontSize: "large"intending a font size token, butfontSize.largedoesn't exist andspacing.largedoes. Lenient resolution would pick the spacing token (wrong segment, wrong value). Strict per-property lookup refuses to cross-resolve and reports the miss. - Unknown CSS keywords. You type
color: "blak"instead of"black"or"inherit". It's not a token, not raw-looking, not a whitelisted keyword — so it errors.
Valid configurations continue to work unchanged. The validation is additive: it only rejects values that were previously producing silently wrong output.
Not all properties are meaningful on every element, but the generator doesn't restrict combinations. Here's a guide to typical usage:
| Element | Typography | Color | Background | Hover |
|---|---|---|---|---|
body |
Yes | Yes | Yes | — |
heading |
Yes | Yes | Yes | — |
h1–h6 |
Yes | Yes | Yes | — |
caption |
Yes | Yes | Yes | — |
button |
Yes | Yes | Yes | — |
link |
Yes | Yes | Yes | hoverColor |
The SCSS generator uses :where() selectors for all elements except body. This keeps specificity at zero, making it easy to override base styles in component CSS without needing extra specificity.
| Element | SCSS Selector |
|---|---|
body |
body { } |
heading |
:where(h1, h2, h3, h4, h5, h6) { } |
h1–h6 |
:where(h1) { } ... :where(h6) { } |
caption |
:where(figcaption) { } |
button |
:where(button) { } |
link |
:where(a) { } and :where(a:hover) { } |
:where() gives selectors zero specificity. This means component BEM classes always win without ordering tricks:
| Selector | Specificity |
|---|---|
:where(h2) |
0,0,0 |
.mylib-card__title |
0,1,0 |
A heading inside a Card component gets the Card's styles, not the base typography. A bare heading outside any component gets the base styles.
All base styles are written to base-styles.scss in the output.srcDir directory. This file is auto-generated and should not be edited manually.
Content styles are split into two files:
Generated — base-styles.scss contains config-driven element typography. Regenerated every time you run the generator.
Authored — content.scss is a file you own. It imports the generated file and adds behavioral rules that don't belong in a config:
// content.scss — you own this file
@use 'base-styles';
:where(p, ul, ol, blockquote, pre, figure, table) {
margin-block: 0 1em;
}
:where(ul) {
list-style: disc;
padding-inline-start: 1.5em;
}
:where(ol) {
list-style: decimal;
padding-inline-start: 1.5em;
}The generator only writes base-styles.scss. Your content.scss is never touched.
The Storybook preset auto-injects content.scss when it exists. See Storybook Preset for details.
Base styles map to styles.typography, styles.color, styles.spacing, and styles.elements in theme.json. WordPress applies these as global styles in the Site Editor.
Apply .has-global-padding to content containers and .alignfull to children that should break out:
<div className="has-global-padding">
<h1>Page Title</h1>
<p>Content with root padding.</p>
<div className="alignfull">
<img src="hero.jpg" alt="Full-width hero" />
</div>
<p>Back to padded content.</p>
</div>Layout components apply WordPress layout classes so spacing works in both Storybook and WordPress:
<section className="mylib-section is-layout-constrained">
{children}
</section>
<div className="mylib-grid is-layout-grid">
{children}
</div>Block gap is a container-level concern — individual components (Card, Button, etc.) don't use it.
Components never rely on base styles. Every component owns its own typography:
.mylib-card {
&__title {
font-size: var(--mylib--font-size-x-large);
font-weight: 700;
line-height: 1.5;
}
&__content {
font-size: var(--mylib--font-size-medium);
line-height: 1.5;
}
}A Card works identically whether it's inside prose content or not. Base styles are for bare elements in content areas — components don't need them.
Here's a complete baseStyles section with all supported elements, assuming matching tokens exist for every reference:
{
"baseStyles": {
"body": {
"fontFamily": "inter",
"fontSize": "medium",
"fontWeight": "normal",
"lineHeight": "normal",
"color": "black",
"background": "grey-faint"
},
"heading": {
"fontFamily": "inter",
"color": "primary"
},
"h1": { "fontSize": "5x-large", "fontWeight": "medium" },
"h2": { "fontSize": "4x-large", "fontWeight": "medium" },
"h3": { "fontSize": "3x-large", "fontWeight": "medium" },
"h4": { "fontSize": "2x-large", "fontWeight": "medium" },
"h5": { "fontSize": "x-large", "fontWeight": "medium" },
"h6": { "fontSize": "large", "fontWeight": "medium", "fontStyle": "italic" },
"caption": {
"fontSize": "small",
"fontStyle": "italic",
"fontWeight": "light",
"color": "warning"
},
"button": {
"color": "white",
"background": "primary"
},
"link": {
"color": "primary",
"hoverColor": "error"
},
"spacing": {
"blockGap": "medium",
"padding": {
"top": "0",
"right": "large",
"bottom": "0",
"left": "large"
}
}
}
}This config requires the following tokens to exist (typical for a full design system):
tokens.color:primary,black,grey-faint,white,warning,errortokens.fontFamily:intertokens.fontSize:small,medium,large,x-large,2x-large,3x-large,4x-large,5x-large(the larger sizes are typicallycssOnly: trueso they stay out of the Gutenberg size picker)tokens.fontWeight:light,normal,mediumtokens.lineHeight:normaltokens.spacing:medium,large
If any of these are missing, c2b generate will fail with a clear error pointing at the first offending reference.