Props, Attributes & the .props Contract
Dim components receive data from the parent as HTML attributes, the .props property, and light-DOM children. Understanding which channel to use prevents silent bugs and security issues.
Three input channels
| Channel | Best for | Example |
|---|---|---|
| Attributes | Strings, numbers, booleans, JSON objects | transitionId="3" |
.props | Functions, live objects, non-JSON data | .props=${{ onSave, user }} |
| Children | Composition / slots | <my-card>Hello</my-card> |
Inside define(), Dim merges attributes and .props into a single props object passed to your functional component.
Case-insensitive prop names
HTML lowercases attribute names. Dim resolves props case-insensitively:
// Parent sets attribute transitionId — stored as transitionid in DOM
html`<page-view transitionId="${id}"></page-view>`
// Inside component, both work via readProp():
props.transitionId === props.transitionid
Prefer .props for camelCase-heavy APIs to avoid ambiguity.
JSON object attributes
The Dim html template helper supports React-like attr={{ ... }} syntax, but values are parsed with JSON.parse only — not evaluated as JavaScript:
// Valid JSON — works
html`<todo-item todo='{"id":1,"text":"Milk","done":false}'></todo-item>`
// Invalid — functions, undefined, trailing commas, unquoted keys
html`<todo-item todo={{ id: 1, onToggle: toggle }}></todo-item>`
// → console warning; use .props instead
Lit also auto-parses attribute values that start with { or [ when set as plain attributes.
Passing functions and callbacks
Functions must use property binding, not attributes:
const handleDelete = (id) => setTodos(todos.filter((t) => t.id !== id));
return html`
<todo-list
.props=${{
todos,
onDelete: handleDelete,
}}
></todo-list>
`;
Attributes are always strings; a function stringified to the attribute "function …" cannot be called.
The .props property
.props sets the Lit reactive property props: Object on the custom element:
html`<my-form .props=${{ values, errors, onSubmit }}></my-form>`
On each render, Dim merges:
- Parsed attributes
- Keys from
this.props(.propswins on conflict via case-insensitive lookup)
Use .props when passing:
- Event handlers (
@clicktargets in child templates) - Class instances or circular structures
- Values that change every render without attribute serialization
sharedDependencies injection
Components receive hooks twice — import from dim.ts or use the second argument:
const Child = (props, { useState, html, useRef }) => { ... };
define({ tag: "my-child", component: Child });
Demos often prefer the injected object so federated or lazy-loaded modules share the same hook instances.
Additional injected helpers:
querySelector— scoped to the component shadow rootgetRef(refName)— resolve another component’suseReftargetrenderChildren— slot / HTML child contentkeyed— Lit directive for stable list keys
Children and slots
Light DOM children are captured in connectedCallback:
const Wrapper = (props, { html, renderChildren }) => {
return html`
<div class="wrapper">
${renderChildren(props.children)}
</div>
`;
};
Strings with custom element tags use <slot>; plain HTML may use unsafeHTML internally.
See 14. Composition patterns (planned) or Storybook nested components demo for advanced slot usage.
transitionId and other framework props
Framework features read props before your component runs:
html`
<screen-host
transitionId="${viewId}"
transitionDuration="400"
transitionAutoDirection="false"
.props=${{ viewId, data }}
></screen-host>
`;
transitionId on attributes triggers auto view transitions even without .props.
Checklist
- Primitives → attributes or JSON in templates
- Functions / objects with methods →
.props - camelCase API → prefer
.props - Never pass secrets in attributes (visible in DOM inspector)
- Encrypted state →
useStorewith{ encryptionKey }, not props
Conclusion
The .props contract is Dim’s escape hatch for everything HTML attributes cannot represent. JSON-only attribute parsing is a deliberate security and performance choice.
- 2. Todo list tutorial — first
.propsexamples - MIGRATION.md