Conventions #
TypeScript #
Use strict TypeScript. Import types with import type. Local imports include the explicit .js
extension.
Formatting #
oxfmt owns formatting. The repo uses tabs, 100-column wrapping, single quotes in TypeScript, and
double quotes in JSX.
Linting #
oxlint owns linting (via vp lint). To suppress a rule, explain why in a plain comment, then put
// oxlint-disable-next-line <plugin>/<rule> directly above the offending line — the directive
applies to the literal next line, so it cannot sit inside a multi-line comment.
Reuse #
Prefer utilities the repo already ships over hand-rolling: cx from @luke-ui/react/utils for
class names, zod schemas for validating untrusted data (URL state, postMessage payloads, results
from JSON.parse), and react-error-boundary in the docs app for error boundaries.
Author sample or template code as real .tsx files imported with ?raw so it stays typechecked,
not as inline string constants.
Naming #
- Components:
PascalCase, for exampleIconButton. - Props:
PascalCaseProps, for exampleButtonProps. - Files:
kebab-case, for exampleicon-button.tsx. - CSS:
*.css.ts.
Code style #
- Prefer
.map()for one-to-one transforms. Prefer.filter()for selection. Prefer.flatMap()when filtering and mapping are combined. In.flatMap(), use an early return for the filtering case unless a ternary is clearly easier to read. - Prefer
for...ofwhen a transform needs more explicit control or mutation. Preferfor...ofover.reduce. Use.reduceonly when it makes the operation substantially clearer. - Prefer
MaporSetfor repeated lookup or deduplication. Do not repeatedly scan arrays inside loops. - Prefer
functiondeclarations for named helpers. Use an arrow when a short implicit return is clearer, or when assigning a named function type. Prefer an explicitreturnwhen an arrow body wraps across multiple lines. - Prefer early returns when a branch finishes the current function.
- Prefer a ternary for a simple two-way branching assignment. Do not nest ternaries. Multiple independent simple ternaries are fine.
- For three or more branches on the same value, use
switchwhen it makes the branches easier to scan. Flat early returns are also fine. For a pure key-to-value mapping, use aRecordor lookup object. Consider whether a value modelled as a boolean should actually be a union, in which case a lookup object is often the better design. - Prefer
for...ofover.forEachfor side-effect iteration, especially whenbreak,continue, or early exit helps. - Put the exported entry point first. Put supporting constants and helpers below it. If a helper uses a non-hoisted constant, place that constant before the helper.
- Keep branching or search mutation scoped with an inline IIFE when that improves locality. If only one value escapes, return that value directly. If multiple values escape, return an object.
- Extract multi-step object construction into its own function only when the construction would otherwise mix with unrelated logic.
- Define every static regex as a named module-scope
SCREAMING_SNAKE_CASEconstant with a_PATTERNsuffix, including trivial or one-use regexes. Regexes withgoryare stateful when used with.test()or.exec(), because they mutatelastIndex.
Testing #
See TESTING.md for test type, placement, and writing rules.
Short version:
- Use the smallest test surface that proves the behaviour.
- Colocate tests with the source they cover.
- Test through public APIs and role-based queries.
- Start bug fixes with a failing test that reproduces the bug.
Components #
Components wrap react-aria-components and use composeRenderProps for styling.
Element choice #
Use elementType to change the semantic element without changing the accepted DOM props. Accept
only the elements a component is designed to render.
Use a dedicated component for element-specific behaviour or props. Use Link for links and Button
for buttons.
Use renderRoot for Luke UI-owned root replacement when the callback must own the element and its
DOM attributes. Use render<Name> only for a deliberately replaceable named part. React Aria's own
render stays render on primitives and other RAC-owned seams. Keep elementType and renderRoot
mutually exclusive.
Luke UI renderRoot and render<Name> callbacks receive (domProps, state). The first argument
contains resolved presentation props. Pass an empty object as the second argument when the component
has no public render state.
Do not add generic polymorphic props, as, or asChild without a demonstrated need. Apply this
rule to public component APIs, not internal prop handling.
See COMPONENTS.md for component and primitive structure, package paths, and generator rules.
Styling #
See STYLING.md for cascade layers, recipes, styling utilities, and logical CSS rules.
Documentation #
See DOCUMENTATION.md for what to document, writing style, examples, MDX page structure, and docs freshness rules. It also governs JSDoc and code comments.