Property Signals Understanding Properties In addition to attributes, there are also properties. Though often conflated, there is an important distinction: attributes are strings defined in HTML, and properties can be any type of data, strictly in JavaScript and completely invisible to HTML. Modern templating solutions often allow developers to assign properties via HTML attribute syntax, even though they are not actually attributes. While this distinction may seem trivial for those working with modern frameworks, it becomes much more relevant when defining custom elements that may be used in plain HTML. Thunderous itself now supports passing properties via the HTML, but with a distinctive syntax to avoid confusion. For example, count="${0}" will set the count attribute to the string, "0", but prop:count="${0}" will set the count property to the number, 0, and will not be reflected in the HTML. const const MyElement = customElement(() => { return html`<my-counter prop:count="${0}"></my-counter>`; }); PLEASE NOTE: While HTML does not natively support uppercase characters in attributes, Thunderous supports camelCase for properties. For example, prop:myCount="${0}" will set the myCount property to the number, 0, and will not be reflected in the HTML. Properties as Signals Thunderous supports properties for cases where strings are not sufficient. These are also reflected as signals within the component, but the consumer of the component will not directly interact with this signal. myElement.count = 1 will update the internal signal. const MyElement = customElement<{ count: number }>(({ propSignals }) => { // The signal and property are defined automatically when the proxy is accessed const [count, setCount] = propSignals.count; setCount(0); // As of v2.2.0, you can also initialize the signal immediately const [count, setCount] = propSignals.count.init(0); // setCount() will also update the DOM property, // eg. `document.querySelector('my-element').count` return html`<output>${count}</output>`; }); To strongly type the property, pass the property types as a generic to customElement. Without this, the signal's type will default to Signal<unknown>. NOTICE: The signal's getter will throw an error at runtime if the property is accessed before its value is initialized. You must either set the property before the signal is accessed or call init() on the signal. Syncing Attributes With Properties There is also a way to sync attributes with properties by coercing the strings into the desired type, though it should be used deliberately, with caution. It's best not to use this to parse JSON strings, for example. To use this feature, pass attributesAsProperties in the options. It accepts an array of [attributeName, coerceFunction] pairs. For primitive types, you can use their constructors for coercion, like ['count', Number]. PLEASE NOTE: Kebab case converts to camelCase, so ['my-count', Number] will map to propSignals.myCount. This is done because HTML attributes cannot support uppercase characters. const MyElement = customElement<{ count: number }>(({ propSignals }) => { const [count, setCount] = propSignals.count.init(0); // setCount() will update both the DOM property, AND the HTML attribute. return html`<output>${count}</output>`; }, { attributesAsProperties: [['count', Number]] }); With the above snippet, count may be controlled by setting the attribute, like so: <my-element count="1"></my-element> ...and the attribute will reflect changes made to the property as well: const myElement = document.querySelector('my-element'); myElement.count = 1; In both cases, the count() signal will be updated.