vroqjs.com

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 + onChange
  • checked + onChange
  • open + 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:

  • PrimaryButton
  • DangerButton
  • GhostButton
  • H1 to H6

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