None of these are enforced by tooling. They're the shape every file already has, and the reason the machinery in A page's life works without manual wiring. Break one and the failure is usually silent — the component just never loads. Use this page as a review checklist.
The shape of a component
Here is the whole Border Component — small enough to see every rule at once. It's the
reference theme's notched-border (Novaa-only), but the structure is universal:
import './Border.scss'
import { Instantiable } from '../../utilities/Instantiable/Instantiable.js'
import { Traits } from '../../utilities/Traits/Traits.js'
class Border extends Instantiable {
static selector = '.notched-border'
}
const Final = new Traits(Border, [])
export { Final as Border }
The checklist
Colocate the three halves. A component is a view (views/{type}/name.php, kebab-case)
plus an asset folder (assets/{type}/Name/, PascalCase) holding exactly its Name.js and
Name.scss. Nothing else lives in that folder; that folder holds nothing else.
Import the SCSS as a side-effect, first, at the top of the JS.
import './Border.scss'
This is how a component's styles enter the bundle — the import() that loads its JS pulls
the CSS along with it. No stylesheet is registered anywhere else. If the JS doesn't import
the SCSS, the styles never ship.
Extend Instantiable. Every element-backed behaviour extends it. It gives you the
element → instance registry, data-* parsing into options, and the lifecycle. A
component with no behaviour still extends it (a CSS-only component) purely so its JS
import can pull the SCSS.
Declare a static selector. This string is how the AssetLoader and every host find
the element. It is the entire presence gate — no selector, no loading.
static selector = '.notched-border'
Apply Traits once, at the end, and export the result. The last two lines of every
file are the same shape — compose the capabilities into the class, then export it:
const Final = new Traits(Class, [ /* AutoInitializes, HasImage, FadesIn, … */ ])
export { Final as Class }
Add a capability by editing the array, never by subclassing. An empty array ([]) is fine
for a component with no traits.
The export alias must equal the registered name. export { Final as Border } works
because the loader descriptor says name: 'Border' and resolution does
exports['Border']. Rename one without the other and resolution throws at load. See
The Globals registry for the name/moniker rules.
Wire behaviour in async init(), and make it idempotent. init() is where real
interactions bind — it runs after everything you declared in boot has loaded. Hosts
call autoInitializeComponents() here to init their sub-components in order, then
await super.init(). The lifecycle guards init() with isInitialized, so it's safe to
call more than once.
async init () {
await this.autoInitializeComponents()
await super.init()
}
Never define styles in JS. JavaScript toggles state classes — is-open,
is-animated, is-settled, loaded — and the colocated SCSS owns what each one looks
like. No inline styles, no style objects, no CSS strings in JS.
"No styles in JS" is not "no JS." Writing behaviour is the job. The line is narrow: JS
flips a class; SCSS decides what the class means. If you find yourself setting
element.style, the rule you want is probably a state class plus a SCSS block.
Let a Field emit editor styles. Editor choices for colour, spacing, and borders
reach the frontend as CSS custom properties and utility classes emitted by PHP Field
classes — never as hard-coded values in your SCSS or JS. Your SCSS consumes those
variables. See The Backend Bridge.
Related
- Traits — the
new Traits(Class, [...])factory in depth. - Instantiable & the instance model — the base every file extends.
- The Globals registry — name, moniker, and the export-alias rule.
- The state-class contract — JS drives state, SCSS expresses it.