@prop
@prop(...) declares a reactive property on a Radiant host.
On RadiantElement, use it for values that belong to the custom element's external API and may need attribute sync, type conversion, reflection, or JSX bindings.
On RadiantController, @prop(...) instead exposes a host property channel. That lets surrounding JS assign real values like objects or arrays directly on the attached host element without serializing them into attributes.
For a RadiantElement, the HTML attribute defaults to the kebab-case property name (minWidth becomes min-width). Set attribute to use a different name; Radiant preserves that configured spelling. HTML normalizes attribute names to lowercase and matches observed names exactly, so use a lowercase attribute value when consumers author the attribute in HTML. JSX keeps the camelCase property name, so <RuiSidebar minWidth={200} /> maps to the same min-width HTML attribute during SSR.
Example
import { RadiantElement, customElement, prop } from '@ecopages/radiant';
@customElement('user-card')
export class UserCard extends RadiantElement {
@prop({ type: String }) name!: string;
@prop({ type: Number, defaultValue: 0 }) visits!: number;
@prop({ type: Boolean, reflect: true, defaultValue: false }) active!: boolean;
}The declared field type must agree with type, defaultValue and transform: see Typing Decorated Fields.
SSR Property Staging
Before the first server render, assign properties through renderComponent (or on a disconnected host before calling renderRadiantElementHostToString):
import { renderComponentToString } from '@ecopages/radiant/server/render-component';
const html = await renderComponentToString(UserCard, {
initialize: (element) => {
element.visits = 7;
},
});Those staged values are serialized into the SSR host tag and used for the first render. This is the supported way to pass non-default props into SSR without attributes.
Controller Host Props
RadiantController can use the same decorator when the input should come from a real host property instead of markup.
import { RadiantController, controller, prop } from '@ecopages/radiant';
import { startControllers } from '@ecopages/radiant/controller-registry';
type ResultsListProps = {
items: Array<{ id: string; label: string }>;
};
@controller('results-list')
export class ResultsListController extends RadiantController {
@prop({ type: Array, defaultValue: [] }) items!: ResultsListProps['items'];
override render() {
return <ul>{this.items.map((item) => <li key={item.id}>{item.label}</li>)}</ul>;
}
}
document.body.innerHTML = '<section data-controller="results-list"></section>';
const host = document.querySelector('[data-controller="results-list"]') as HTMLElement & ResultsListProps;
host.items = [
{ id: '1', label: 'Alpha' },
{ id: '2', label: 'Beta' },
];
startControllers(document);Prefer @ecopages/radiant/controller-registry for startControllers(...) when a module only needs controller activation.
Options
| Option | Type | Description |
|---|---|---|
type | String | Number | Boolean | Object | Array | Default attribute conversion strategy, checked against the field. Array and Object use JSON. |
reflect | boolean | Write property changes back to the host attribute. Without it, an authored attribute stays on the host and is read at upgrade, but later property writes do not touch it. |
attribute | string | false | Override the attribute name, or pass false to make the prop property-only (see Property-only props). |
defaultValue | T | Default property value when the attribute is absent. Pass a fresh array or object on each decorator call. null is accepted. Without transform.fromAttribute, a default that does not match type throws when the class loads. |
bind | boolean | string | Expose a JSX binding companion such as $count or a custom binding name. |
transform | PropTransform<T> | Override attribute conversion and optional JS writes. fromAttribute parses the attribute and owns the field type, so type is no longer checked against it. |
Property-only props
Every @prop has an attribute channel by default: server rendering writes its value into the markup, and the Object and Array converters use JSON.stringify. That is wrong for a live object such as a class instance. Its own fields leak into the HTML (a privacy and size problem), its methods are lost when the client parses the attribute back into a plain object, and a cyclic value makes JSON.stringify throw during SSR.
Pass attribute: false for a prop that only ever travels as a JavaScript property:
@customElement('x-renderer')
class XRenderer extends RadiantElement {
@prop({ type: Object, attribute: false }) engine?: FormEngine;
}A property-only prop has no observed attribute, ignores an authored attribute of the same name, is never reflected and is never serialized by server rendering. reflect: true with attribute: false throws. A value assigned before the element is defined is adopted, and it wins over an authored or server-rendered attribute.
In development, server rendering warns once per tag and prop when a prop that keeps its attribute would serialize a class instance, a function or a cycle, and points to attribute: false:
[@ecopages/radiant] "engine" on <x-renderer> holds a value JSON cannot represent (a class instance, function or cycle), so server rendering would write a lossy attribute or throw. Declare the prop with `attribute: false` to keep it property-only.Production builds drop the warning. Serialized props are public markup: do not keep private or large data in a prop that keeps its attribute.
Custom converters
Default type: Array / type: Object attributes are JSON (value='["a","b"]'). Use transform when the markup protocol is different, such as comma-separated tokens:
import { RadiantElement, customElement, prop, type PropTransform } from '@ecopages/radiant';
const csvStrings: PropTransform<string[]> = {
fromAttribute: (value) => (value ? value.split(',').map((token) => token.trim()).filter(Boolean) : []),
toAttribute: (values) => (values.length > 0 ? values.join(',') : null),
fromProperty: (value) => {
if (Array.isArray(value)) return value.map(String);
if (typeof value === 'string') return value ? value.split(',') : [];
return [];
},
};
@customElement('token-select')
export class TokenSelect extends RadiantElement {
@prop({ type: Array, reflect: true, transform: csvStrings, defaultValue: [] })
value!: string[];
}fromAttributeruns when the HTML attribute is present, including first connect.fromPropertyruns on JS and JSX assignment, including values set before upgrade.toAttributereturningnullor''omits the reflected attribute. That is how an empty selection stays off the host tag. Reflection does not write the resulting attribute removal back into the property:element.value = []remains[].
Without transform, element.value = ['a', 'b'] still works as a JS property write; reflection would stringify JSON, not CSV.
How It Works
@prop(...) uses the property reactivity machinery underneath:
- reads the attribute into the declared property type; an authored attribute stays on the host
- writes the property back through the converter when reflection is enabled
- stores reactive metadata for SSR host serialization
- notifies
@onUpdatedlisteners when the member state changes - can expose a subscribable JSX binding companion
On RadiantController, the same decorator reads and writes a real property on the attached host element instead of going through attribute serialization.
That means this split is intentional:
- use
@attr(...)when the value should stay in markup - use
@prop(...)when JS code should pass structured values directly
JSX Binding Defaults
When you omit bind, Radiant exposes companion bindings by default.
So this:
class CounterCard extends RadiantElement {
@prop({ type: Number, defaultValue: 0 }) count!: number;
}automatically gives you this.$.count and this.bindings.count at runtime. To type them, pass the binding shape as the host's type argument: see RadiantElement.
When To Use It
- Use
@prop(...)for public custom-element API. - Use
@prop(...)onRadiantControllerwhen surrounding JS should pass values through the host element as real properties. - Use
@statefor internal mutable component state.