Conventions
These conventions keep contracts stable across core components and framework adapters.
Naming
- Attributes use kebab-case (
open,aria-controls). - Properties use camelCase (
readOnly,selectedIndex). - Shared state names:
open,disabled,selected,value,invalid,readOnly.
Typed HTML attributes
At an ordinary HTML page boundary, every authored attribute begins as text. Looma attribute strings are coerced according to the declared property type before a controller receives them. This creates parity with native HTML controls, whose element contracts determine how authored attributes become typed values and reflected properties.
- Strings remain strings.
- Number and integer declarations reject non-numeric values instead of silently passing strings.
- Enum declarations accept only their documented values.
- A bare boolean attribute means
true; explicit="true"and="false"values coerce to their corresponding booleans. Omitting the attribute uses the declared default, which must befalseunless a reviewed UX requirement documents the exception. - Lists, records, objects, and functions are property-only inputs because HTML has no lossless, native syntax for them.
Expression syntax such as from:modal="false" belongs inside a Declarative Component template. It is
not consumer syntax for an HTML page; page authors write <ui-dialog modal="false"> or omit
modal.
Events
Event names describe the interaction, not the package or component that emitted it. The event target
already identifies its owner, so prefixes such as looma-editor-* make equivalent interactions
needlessly different across packages.
- Use
inputfor continuous value updates while a user edits. - Use
changefor committed form-control state. - Use
selectfor committing a choice from a menu, list, tab set, or other selection model. - Use
highlightwhen the active option moves without committing it. - Use
openandclosefor visibility lifecycle transitions. - Use
actionfor an application-owned command that the component requests but does not execute. - Use a domain event such as
insertonly when its payload represents a distinct operation rather than ordinary selection or state change.
Events bubble and cross component boundaries. Their detail records the resulting state or requested
intent plus a trigger when keyboard, pointer, and programmatic origins are meaningful. Components
own interaction mechanics; applications own domain mutations requested by action events. Custom
event payload keys are semver-protected.
State ownership
- Raw HTML exposes one native-like state name:
value,checked,open, orediting. - Framework adapters may offer controlled and initial-value ergonomics in framework-native casing,
but those concepts do not add
default-*attributes to the declarative HTML contract. - User interaction updates the component's internal state and emits the documented native or custom event. Applications may write a new property value in response when they own that state.
SSR and Lowering Contract
- SSR HTML must be meaningful before JS loads.
- Lowering may replace the
ui-*invocation wrapper with its declared native root, but must preserve authored semantic descendants and slot meaning. - Required ARIA relationships should be derivable from SSR markup.
Spacing Rule
Components do not set external margins. Layout primitives own inter-component rhythm via gap.
Styles and typography
Every component renders light-DOM native elements and preserves the content you author through its slots.
- A component styles its own root and parts. Everything it does not set, such as the font family, inherits from the page as it would for any element.
tokens.cssputs its values in@layer tokensand its one base rule in@layer base, so unlayered theme CSS overrides them. Component styles are unlayered: change a component through its hooks, not by competing with its selectors.- Components that present text or controls (Text, Button, Input, Badge, and so on) set the size, weight, and colour their design needs from the global type and colour tokens. Theme them through those tokens and the component's hooks, never by selecting the markup a runtime renders.
Where optical text trimming is enabled, set text-box-trim: trim-both and
text-box-edge: cap alphabetic together on the element that owns the text.
Keep component-specific size and leading. Trimming does not inherit, so a trimmed
Button does not activate trimming on a nested Badge. The edge metric can inherit;
declare both settings on each text owner to keep its geometry independent. Do not
force trim inheritance globally or use offsets for individual letters. Badge text
uses the shared trim rule and clips only horizontal overflow for ellipsis; vertical
ink stays visible, preserving ascenders and descenders. Fixed Badge marks retain
visible overflow. Native inputs keep full font metrics to preserve caret geometry.
See the CSS text-box specification.
To give a page Looma's type, set it on the page itself:
body {
font-family: var(--ui-font-sans);
font-size: var(--ui-font-size-md);
line-height: var(--ui-line-height);
color: var(--ui-text);
}