99-reference/component-api-conventions.md
Component API conventions
Component API conventions
This file defines the public component API rules for Vroq v02 during the component unification work.
Use this file together with 99-reference/component-vocabulary.md.
Main goal
The public component API should be easy to:
- remember
- document
- validate
- generate correctly with LLMs
- migrate incrementally inside
vroqjs/v02/components
Allowed public call shapes
All public components should follow one of these two shapes:
Component({ ...props })
Component({ ...props }, ...children)
These are the only canonical public forms that docs and demo apps should teach.
Rule 1: props object first
Every public component starts with a props object.
All migrated components should validate their props using the shared helper:
import { validateProps } from "/vroqjs/system/ui/props.js"
validateProps("ComponentName", props, allowedProps)
Each component should define an explicit allowedProps contract so LLMs can quickly understand the supported API.
Example:
const allowedProps = {
text: { type:"content", required:true },
tone: { type:"string", values:["text","muted","primary","danger"] },
size: { type:"string", values:["h1","h2","h3","h4","h5","h6","body"] }
}
This allows components to fail early when an unsupported prop is used and keeps component APIs consistent across the framework.
Good:
Text({ text: "Hello" })
Button({ label: "Save", onClick })
TextInput({ value: name(), onChange: name })
Card({ pad: "md" }, childA, childB)
CardTitle({ text: "Title" })
CardSubtitle({ text: "Details" })
Avoid:
Text("Hello")
Button("Save", onClick)
TextInput(name, { label: "Name" })
Tabs(tab, items, { variant: "underline" })
CardTitle("Title")
CardSubtitle("Details")
Rule 2: positional children only for container-like components
Container and layout components may take positional children after the props object.
Good:
VStack({ gap: "md" }, childA, childB)
HStack({ gap: "sm" }, childA, childB)
Card({ pad: "lg" }, childA, childB)
ScrollView({ pad: "lg" }, child)
Do not move children into a children prop unless a component has a strong technical reason.
Rule 3: leaf and widget components use named content props
Components that are not containers should use named props for visible content.
Good:
Text({ text: "Hello" })
Badge({ text: "New", tone: "primary" })
Button({ label: "Save", onClick })
Checkbox({ checked, onChange, label: "Done" })
Avoid:
Text("Hello")
Badge("New", { tone: "primary" })
Button("Save", onClick)
Checkbox(done, { label: "Done" })
Rule 4: interactive widgets should be controlled through named props
Use explicit controlled props.
Preferred patterns:
value+onChangechecked+onChangeopen+onOpenChange
Good:
TextInput({ value: name(), onChange: name, label: "Name" })
Checkbox({ checked: done(), onChange: done, label: "Done" })
Tabs({ value: tab(), onChange: tab, items })
Segmented({ value: mode(), onChange: mode, items })
Avoid public APIs that depend on:
- binding accessor as first positional argument
- multiple overloads for the same component
- positional meaning that differs by component family
Rule 5: docs should show one canonical usage style
Docs should not teach multiple public calling styles unless a component is still in migration.
During migration:
- component implementation may temporarily support old signatures
- docs should prefer the new canonical signature
- demo app examples should move to the new canonical signature as soon as the component is migrated
Rule 6: wrappers and aliases are secondary
Helper wrappers may exist, but they should not replace the main public API.
Examples:
PrimaryButtonDangerButtonGhostButtonH1toH6
Rules:
- document the base component first
- treat wrappers as aliases or convenience helpers
- do not let wrappers multiply the conceptual API surface
Rule 7: prefer family consistency over local convenience
When designing or refactoring a component, first ask how its family should behave.
Examples:
- all input-like widgets should prefer object-first controlled props
- action-like widgets should prefer
label,icon,onClick - text-like widgets should prefer
text,tone,size - container-like widgets should prefer props-first plus children
Rule 8: public API should not depend on reading source to know argument order
The old framework often required reading component source to confirm whether a component used:
- text first
- binding first
- props first
- props plus children
- multiple overloads
The new public rule removes that ambiguity.
A fresh LLM should usually be able to guess the correct call shape from this file and the vocabulary doc.
Rule 9: framework migration strategy
For v02 migration work:
1. keep behavior stable when practical 2. refactor components in place in vroqjs/v02/components 3. keep the demo app working after every step 4. if a migrated public component is missing from demo/r01, add it 5. update docs as each component family is migrated 6. prefer a temporary compatibility layer over breaking the demo blindly 7. remove legacy signatures after demo and docs use the new canonical form
Rule 10: public UI surface should come from ui.js
The preferred app import surface is:
import { Text, Button, Card, VStack } from "/vroqjs/ui.js";
ui.js should export the public components that normal apps are expected to use.
Direct imports from components/*.js are acceptable when:
- working on framework internals
- testing an implementation file directly
- using a component that has not yet been added to
ui.js
Final rule
A component API is good when:
- props are named clearly
- call shape is predictable
- docs can explain it in a few lines
- demo examples use the same form
- LLMs are unlikely to guess the wrong signature