---
title: "@prop"
description: "Use @prop for public reactive host inputs."
group: Decorators
order: 3
---
# @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
```typescript
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`):
```typescript
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.
```typescript
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
{this.items.map((item) => - {item.label}
)}
;
}
}
document.body.innerHTML = '';
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. `Array` and `Object` use JSON. |
| `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. Pass a fresh array or object on each decorator call. |
| `bind` | `boolean \| string` | Expose a JSX binding companion such as `$count` or a custom binding name. |
| `transform` | `PropTransform` | Override attribute conversion and optional JS writes. |
## 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:
```typescript
import { RadiantElement, customElement, prop, type PropTransform } from '@ecopages/radiant';
const csvStrings: PropTransform = {
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[];
}
```
- `fromAttribute` runs when the HTML attribute is present, including first connect.
- `fromProperty` runs on JS and JSX assignment, including values set before upgrade.
- `toAttribute` returning `null` or `''` **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
- writes the property back through the converter when reflection is enabled
- stores reactive metadata for SSR host serialization
- notifies `@onUpdated` listeners 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:
```typescript
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(...)` on `RadiantController` when surrounding JS should pass values through the host element as real properties.
- Use `@state` for internal mutable component state.
See [@state](/docs/decorators/state) and [@attr](/docs/decorators/attr).