What a brand actually owns
More than color. A brand config carries its intents, but also its elevation steps,shape.radius-base, typography families and ratio, motion.duration-base and easing, space.unit, its flexibility policy bounding every user preference axis, its presets (including which mode and density levels exist at all), plus optional gradients, ramps, materials, and system overrides. See the brand config overview for the full schema.
What a brand does not own is the solving. APCA lightness resolution, the scheme axis, CVD and warmth compensation, and the build-failing accessibility gate run identically for every brand. That is why a new brand inherits correct contrast behavior from its first generation rather than earning it.
Project structure
Brands live in directories undersrc/brands/. There are two shapes, and the directory contents decide which one applies. Here is how a workspace with two fictional brands — the Aurora Airways family and the standalone Acme brand — would be laid out:
config.global.yaml is a family: each subdirectory holding a config.yaml becomes its own brand, with the merged slug {parent}-{sub} — aurora-rewards, aurora-booking. A directory containing only config.yaml is a flat brand, and its slug is the directory name.
The engine bundles several demo brand families in this layout for testing and demonstration.
Demo brands only. All brand configurations bundled with the Substrate engine, and all brand names used in examples throughout these docs, are fictional demonstrations created to exercise the engine. They do not represent clients or customers of Substrate or its authors, and no affiliation with, sponsorship by, or endorsement from any real company is implied.
There is no workspace file, no brand registry, and no registration step. Discovery is a filesystem scan of the brands directory — a brand exists because its directory does. Adding one is covered in Adding a Brand.
Generating
Generation runs from the engine checkout and covers every discovered brand in one pass:npm run generate:tokens, npm run generate:ramps, npm run generate:descriptors, and npm run generate:doc-views each handle one artifact family, and most accept a target suffix such as npm run generate:tokens:css or npm run generate:tokens:swift. Every one has a :check variant that verifies the committed output matches what the current configs would produce, which is what CI runs.
Per-brand output
Output lands undergenerated/brands/<family>/<sub>/, one subdirectory per target. For the fictional aurora-rewards:
generated/global/ and are not duplicated per brand. Every generated file carries a .gen. marker in its name and a DO NOT EDIT header. Changes belong in the brand YAML.
Why families are worth using
A family exists so that shared decisions live in one file. Updatingsrc/brands/aurora/config.global.yaml propagates to all five Aurora sub-brands on the next generation; adding an intent there gives every sub-brand that intent automatically. A sub-brand’s own config stays small — a well-factored sub-brand is often around thirty lines of genuine divergence over a shared foundation — and the validator actively flags any line in it that merely restates what it already inherits.
Inheritance
The deep-merge contract, null-delete semantics, and why sub-brand configs contain only deltas.
Adding a Brand
Create a directory, write a config, generate — with no registration step in between.