| name | atomico |
| description | Core entry-point router, orchestrator, and reference manual for all Atomico.js tasks. Contains coding standards, API cheat sheets, architectural guidelines, and validation rules.
|
| license | MIT |
| compatibility | Atomico >=2.0, TypeScript >=5.0 |
| metadata | {"category":"core","priority":"highest"} |
Atomico.js โ Consolidated Development & Validation Guide
Unified reference for building, auditing, and validating Atomico.js web components.
AGENT CONTRACT: You may not respond to a code generation request
without completing the Validation Report in ยง7.
If any check fails, fix the code before responding โ do not report
failures as acceptable output.
1. Hello World Component
Every Atomico component is defined with c() and must return a <host> root element.
import { c, useProp, css } from "atomico";
export const MyCounter = c(
({ label }) => {
const [value, setValue] = useProp<number>("value");
return (
<host shadowDom>
<button onclick={() => setValue(value + 1)}>
{label}: {value}
</button>
</host>
);
},
{
props: {
label: { type: String, value: () => "Increment" },
size: {
type: String,
reflect: true,
value: (): "normal" | "small" => "normal"
},
value: { type: Number, value: () => 0 }
},
styles: css`
:host {
--font-size: 1em;
}
:host([size="small"]) {
--font-size: 0.5em;
}
`
}
);
2. Architectural Rules
- Modular by Default: Avoid monolithic files. Split complex views into a
components/ folder.
- Reuse First: Audit existing workspace components before generating new ones.
- Prop-Driven Communication: Pass data down via props, dispatch events up.
- Separate Registration: NEVER call
customElements.define inside the component file. Centralize all registrations in an index file.
import { MyCounter } from "./my-counter.js";
customElements.define("my-counter", MyCounter);
3. Props
Type system
type AtomicoPropType =
| StringConstructor
| NumberConstructor
| BooleanConstructor
| ArrayConstructor
| ObjectConstructor
| MapConstructor
| SetConstructor
| PromiseConstructor
| (new (...args: any[]) => HTMLElement);
type PropShorthand = AtomicoPropType;
interface PropConfig<T> {
type: AtomicoPropType;
value?: () => T;
reflect?: boolean;
}
Declaration syntax
Use the simplest form that satisfies the requirement:
props: {
label: String,
count: { type: Number, value: () => 0 },
variant: { type: String, reflect: true, value: (): "primary" | "danger" => "primary" }
}
Forbidden:
props: {
name: { type: String },
count: { type: Number, value: 0 }
}
Arrays & Objects โ strict return-type annotation
Without an explicit return type, TypeScript resolves Array to never[] and Object to {}.
Annotate the factory return type to guarantee correct inference in TSX:
interface Option { value: string; label: string; }
interface Config { theme: "light" | "dark"; debug: boolean; }
props: {
options: { type: Array, value: (): Option[] => [] },
config: { type: Object, value: (): Config => ({ theme: "light", debug: false }) }
}
Reflect rules
reflect: true is only for visual / CSS states. Allowed types: String, Number, Boolean.
props: {
disabled: { type: Boolean, reflect: true },
variant: { type: String, reflect: true, value: () => "primary" }
},
styles: css`
:host([disabled]) { opacity: 0.5; pointer-events: none; }
:host([variant="danger"]) { background: red; }
`
Never use reflect: true on Array or Object โ it triggers expensive DOM serialization.
Events & Callbacks
declare function event<Detail = void>(options?: {
bubbles?: boolean;
composed?: boolean;
cancelable?: boolean;
}): EventDescriptor<Detail>;
declare function callback<
Fn extends (...args: any[]) => NonNullable<unknown>
>(): Fn | undefined;
export const ActionButton = c(
(props) => (
<host>
<button onclick={() => props.action({ id: 42 })}>Fire</button>
</host>
),
{
props: {
action: event<{ id: number }>({ bubbles: true, composed: true })
}
}
);
Shadow DOM boundary rule: Native events like change and submit have composed: false
and cannot cross the Shadow DOM boundary. Always declare a custom event() with
{ bubbles: true, composed: true } and dispatch it explicitly.
export const UiSelect = c(
({ change, options }) => (
<host shadowDom>
<select onchange={(e) => change(e.currentTarget.value as string)}>
{options.map((o) => (
<option value={o.value}>{o.label}</option>
))}
</select>
</host>
),
{
props: {
options: {
type: Array,
value: (): { value: string; label: string }[] => []
},
change: event<string>({ bubbles: true, composed: true })
}
}
);
export const TextEditor = c(
({ content, save }) => (
<host shadowDom>
<button
onclick={async () => {
if (save) {
const ok = await save(content);
if (ok) console.log("Saved!");
}
}}
>
Save
</button>
</host>
),
{
props: {
content: { type: String, value: () => "" },
save: callback<(content: string) => Promise<boolean>>()
}
}
);
Forbidden:
props: {
onChange: event();
}
props: {
save: callback<() => void>();
}
const dispatchInput = useEvent("input");
4. State Management
declare function useProp<T>(name: string): [T, (val: T) => void];
declare function useState<T>(init: T | (() => T)): [T, (val: T) => void];
declare function useObjectState<T extends object>(
init: T
): [T, (partial: Partial<T>) => void];
Decision tree
Does the state need to be read from outside (parent / CSS)?
+- YES -> declare it in props
| +- Does the child also write to it? -> useProp<T>()
| +- Read-only? -> direct destructuring ({ myProp })
+- NO -> private state
+- Single value (boolean, string)? -> useState
+- Two or more related values? -> useObjectState<T>
Correct patterns
export const Badge = c(
({ label, variant }) => (
<host shadowDom>
<span class={variant}>{label}</span>
</host>
),
{
props: {
label: String,
variant: { type: String, reflect: true, value: () => "info" }
}
}
);
export const Counter = c(
({ label }) => {
const [count, setCount] = useProp<number>("count");
return (
<host>
<button onclick={() => setCount(count + 1)}>
{label}: {count}
</button>
</host>
);
},
{
props: {
label: String,
count: { type: Number, value: () => 0 }
}
}
);
export const SearchBar = c(() => {
const [state, setState] = useObjectState({ query: "", filter: "all" });
return (
<host shadowDom>
<input
value={state.query}
oninput={(e) =>
setState({ query: e.currentTarget.value as string })
}
/>
</host>
);
});
Forbidden:
const [label] = useProp("label");
const [query, setQuery] = useState("");
const [filter, setFilter] = useState("all");
const filtered = useMemo(() => items.filter(isActive), [items]);
const filtered = items.filter(isActive);
const done = items.length - filtered.length;
5. JSX
Handlers โ Always Inline
The TSX compiler automatically infers the event target type when handlers are inline.
<input oninput={(e) => setState({ query: e.currentTarget.value as string })} />;
const handleInput = (e: any) => setState({ query: e.currentTarget.value });
<input oninput={handleInput} />;
Rule: Single-use handlers go inline. Extract only if the exact same function is
shared across multiple elements.
useRef โ no null initialization
declare function useRef<T>(): { current: T | undefined };
const inputRef = useRef<HTMLInputElement>();
if (inputRef.current) inputRef.current.focus();
const btnRef = useRef<typeof MyButton>();
useListener โ prefer over manual addEventListener
useListener(containerRef, "keydown", (e) => {
if (e.key === "Enter") handleSubmit();
});
useEffect(() => {
const el = containerRef.current;
el?.addEventListener("keydown", handler);
return () => el?.removeEventListener("keydown", handler);
}, []);
JSX type coercions
Atomico JSX maps attribute values differently from standard React/DOM types:
oninput={(e) => setState({ title: e.currentTarget.value })}
oninput={(e) => setState({ title: e.currentTarget.value as string })}
<input value={form.count} />
<input value={String(form.count)} />
const form = e.target as HTMLFormElement;
const form = e.target as unknown as HTMLFormElement;
Composition โ PascalCase Constructors
When a child component's constructor is imported in the file, always use it as a PascalCase tag:
import { TodoItem } from "./todo-item.js";
<TodoItem title="Task" completed={false} />
<todo-item title="Task" />
Fallback: kebab-case string tags are allowed only when the constructor is not
accessible in the current file (native HTML elements, globally registered third-party elements).
6. API Reference
c(render, config): Constructor
Defines the web component constructor. The render function receives props and must return <host>.
css: CSSResult (tagged template)
Tagged template literal for styles encapsulated in Shadow DOM.
-> example
useProp<T>(name: string): [T, (val: T) => void]
Reads and writes a declared prop from inside the component. Use only when the component needs to mutate the value internally.
-> example
useState<T>(init: T | (() => T)): [T, (val: T) => void]
Single ephemeral private state. Use only for one isolated boolean/string toggle.
-> example
useObjectState<T extends object>(init: T): [T, (partial: Partial<T>) => void]
Grouped private state with partial updates. Use when managing 2+ related values.
-> example
event<Detail>(opts?): EventDescriptor<Detail>
Declares a Fire-and-Forget CustomEvent dispatcher in props. Name WITHOUT "on" prefix.
-> example
callback<Fn extends (...args) => NonNullable<unknown>>(): Fn | undefined
Declares a Request-Response callback prop. Fn must never return void.
-> example
useEvent<Detail>(name: string, opts?: EventInit): (detail?: Detail) => void
Dispatches a CustomEvent from within component logic or a custom hook.
-> example
useListener(ref, type: string, callback, opts?: AddEventListenerOptions): void
Subscribes to DOM events on a ref. Handles cleanup automatically on unmount.
-> example
useRef<T>(): { current: T | undefined }
Persistent mutable reference to a DOM node or constructor. No null initialization needed.
-> example
useEffect(fn: () => void | (() => void), deps?): void
Executes side effects asynchronously after render/paint.
useMemo<T>(fn: () => T, deps: any[]): T
Caches expensive computations. Do not use for simple operations on small collections.
-> example
useSlot(ref): Element[]
Tracks elements assigned to a <slot>. Triggers re-render on slot changes.
-> example
useNodes(filter?): Node[]
Observes Light DOM children directly via MutationObserver.
-> example
useParent<T>(target, crossShadow?: boolean): T | undefined
Traverses DOM ancestors. Set crossShadow = true to cross Shadow DOM boundaries.
-> example
useInternals(): ElementInternals
Accesses the native ElementInternals instance. Requires form: true in config.
-> example
useFormProps(): [value, setFormValue]
Auto-syncs name and value properties with the parent FormData.
-> example
useFormValidity(check: () => string | void, deps?): void
Integrates native browser constraint validation into form-associated components.
-> example
usePromise<T>(fn: () => Promise<T>, deps?): [T | undefined, "pending" | "fulfilled" | "rejected"]
Resolves and tracks a local promise. The component owns the async lifecycle.
-> example
useAsync<T>(fn, deps?): [T | undefined, "pending" | "fulfilled" | "rejected"]
Manages async operations controlled by parent elements.
-> example
useSuspense(promise: Promise<any>): void
Suspends rendering until the given promise resolves.
-> example
useHost<T>(): { current: T }
Returns a ref pointing directly to the component's host element.
-> example
useUpdate(): () => void
Returns a function that forces an imperative re-render of the element.
-> example
7. Validation Pipeline
Before marking any task as complete, run this two-phase audit.
Phase 1 โ Semantic Linter (read source files, check textually)
| # | Check | Reject if... |
|---|
| 1 | PascalCase constructors in JSX | imported constructor used as kebab-case tag |
| 2 | Inline single-use handlers | handler extracted to a local const handleX = ... variable |
| 3 | No void callbacks | callback<() => void>() found in props |
| 4 | No manual prop types | ({ x }: { x: string }) => found in render signature |
| 5 | useListener over addEventListener | manual addEventListener inside useEffect on a ref |
Fail fast: if any check above fails, report the violation immediately and skip Phase 2.
Phase 2 โ Compiler Verification
npx tsc -p tsconfig.json --noEmit --noErrorTruncation > tmp/tsc-errors.log 2>&1
- If no
tsconfig.json exists, generate a fallback targeting ["src/**/*", "types/**/*"].
- Open
tmp/tsc-errors.log and send the complete error list to the Developer.
- The Developer and Validator feedback loop is capped at 3 iterations. Escalate to the user if unresolved.
Required Validation Report
After every code generation, append this block to the response:
### Validation Report
- [ ] All handlers are inline (no extracted const handleX)
- [ ] Imported constructors use PascalCase in JSX
- [ ] No callback<() => void> in props
- [ ] Array/Object props have return-type annotation
- [ ] tsc: `npx tsc --noEmit` -> PASS / FAIL - [error summary if FAIL]