@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.
Example
import { RadiantElement, customElement, prop } from '@ecopages/radiant';
@customElement('user-card')
export class UserCard extends RadiantElement {
@prop({ type: String }) declare name: string;
@prop({ type: Number, defaultValue: 0 }) declare visits: number;
@prop({ type: Boolean, reflect: true, defaultValue: false }) declare active: boolean;
}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: [] }) declare items: ResultsListProps;
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 | Attribute conversion strategy. |
reflect | boolean | Reflect property changes back to the host attribute. |
attribute | string | Override the attribute name. |
defaultValue | T | Default property value when the attribute is absent. |
bind | boolean | string | Expose a JSX binding companion such as $count or a custom binding name. |
How It Works
@prop(...) uses the property reactivity machinery underneath:
- reads the attribute into the declared property type
- 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.
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.