Guidelines
This page explains how to write code in 3AM so other contributors can read it quickly and extend it safely.
The main rule is simple: keep behavior predictable.
Think in project layers
In this codebase, each folder has a clear role. Reusable UI belongs in src/components. Page-specific UI belongs in src/sections. Route-level composition belongs in src/pages. Shared behavior belongs in src/lib.
When a block is reused in multiple places, move it to a component. When it is only for one page, keep it in the section.
Quick decision rule
If a block appears on two pages, treat it as a component candidate.
Keep render() deterministic and side-effect light
render() should only describe UI from current state. It should not register global listeners, timers, or long-running effects.
Problematic Example (unsafe pattern)
render(): DocumentFragment {
window.addEventListener("scroll", this.handleScroll);
return this.tpl`<div>...</div>`;
}This code can register duplicate listeners whenever rendering happens again.
Recommended Example (project pattern)
protected override onMount(): void {
this.cleanup.on(window, "scroll", this.handleScroll, { passive: true });
}
render(): DocumentFragment {
return this.tpl`<div>...</div>`;
}In this version, setup happens in lifecycle, and cleanup is automatic.
Anti-pattern to avoid
Never attach global listeners directly in render().
Keep nesting shallow (max 3 levels)
In TypeScript and CSS logic, do not nest deeper than 3 levels (if, for, callbacks, etc.). If logic goes deeper, extract a helper function or return early.
Exceptions:
- HTML/template markup (
this.tplstructure) can exceed this when needed for semantic layout. - Declarative config/data objects (for example
defineConfig({...}), route maps, static schema/data shapes) can exceed this when deeper object nesting is structural data, not control-flow complexity.
Problematic Example (too deep)
if (user) {
if (user.profile) {
for (const order of user.profile.orders) {
if (order.status === "open") {
processOrder(order);
}
}
}
}Recommended Example (early returns + extraction)
if (!user?.profile) {
return;
}
for (const order of user.profile.orders) {
if (order.status !== "open") {
continue;
}
processOrder(order);
}Use lifecycle and cleanup intentionally
Use onMount() for DOM-dependent setup.
Use this.cleanup.on(...) for listeners and this.cleanup.add(...) for manual disposal callbacks.
Use onDestroy() only when you need explicit teardown logic before normal cleanup runs.
Custom teardown with cleanup.add
protected override onMount(): void {
const observer = new ResizeObserver(() => {
this.rerender();
});
observer.observe(this.element);
this.cleanup.add(() => observer.disconnect());
}For lifecycle internals, see View Lifecycle.
Keep styling in CSS, not in TS strings
For reusable UI, prefer classes plus src/styles/components/*.css.
Problematic Example
super("span", {
attrs: { style: "padding:4px 8px; background:#222; color:#fff" },
});Recommended Example
super("span", { className: "promo-badge", renderMode: "once" });@layer components {
.promo-badge {
padding: 0.4rem 0.8rem;
background: #222;
color: #fff;
}
}This split keeps behavior in TypeScript and design in CSS.
For full styling flow, see Styles.
Keep selectors and class names disciplined
Component and section styles should use class selectors, not raw element selectors.
Use modern CSS and BAM naming with module prefixes (for example, navbar classes start with nav-).
Do not use !important; resolve cascade issues through layers and module-scoped class selectors.
For the required selector, modern CSS, and BAM rules with examples, follow Styles.
Prefer lazy media for heavy assets
The project already has lazy media infrastructure. Use it.
Problematic Example
render(): DocumentFragment {
return this.tpl`<img src="/assets/dusk/dusk_transparent.webp" alt="Dusk" />`;
}Recommended Example
import { LazyImage } from "@components/lazyImage";
render(): DocumentFragment {
return this.tpl`
${new LazyImage({
src: "/assets/dusk/dusk_transparent.webp",
alt: "Dusk",
className: "hero-image",
})}
`;
}Use LazyPicture when each breakpoint has a different file. Use LazyVideo for deferred poster and video source loading.
Performance habit
For hero images, always ask: "Can this be lazy + responsive with LazyPicture?"
For component APIs and performance context, see Ready Components and Performance.
Build small internal utilities when logic is reused
If helper logic repeats in two or more places, extract it into src/lib with a focused name.
A good current example is wait(ms) in src/lib/async.ts, used by boot-loader timing logic. Keep these helpers small, typed, and framework-agnostic when possible.
Problematic Example (duplicated timing logic)
await new Promise((resolve) => window.setTimeout(resolve, 300));Recommended Example (shared utility)
import { wait } from "@lib/async";
await wait(300);If you want to see how the existing app is wired today, check Core System and Hero Carousel.
MDN references
Validation before PR
bun test
bun run build
bun run docs:buildIf bundle size may change:
bun run build:bundle
bun run budget