Helpers
Mount islands, build safe HTML strings without JSX, and render explicitly trusted markup.
Mount islands
Auto-discovers all [data-ilha] elements in the DOM and mounts the matching island from a registry. This is the recommended way to activate islands on a page, especially when using SSR and hydration.
Basic usage
import { const ilha: IlhaFactoryilha, const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<unknown>Counter = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>counter</p>");
const const Card: Island<unknown>Card = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>card</p>");
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<unknown>Counter, type Card: Island<unknown>Card });
Each key in the registry maps to a data-ilha attribute value in the HTML:
<div data-ilha="Counter"></div>
<div data-ilha="Card"></div>
Options
import { const ilha: IlhaFactoryilha, const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<unknown>Counter = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>counter</p>");
const { const unmount: () => void | Promise<void>unmount } = function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount(
{ counter: Island<unknown>counter: const Counter: Island<unknown>Counter },
{
MountOptions.root?: Element | undefinedroot: var document: Document**`window.document`** returns a reference to the document contained in the window.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)document.Document.getElementById(elementId: string): HTMLElement | nullThe **`getElementById()`** method of the Document interface returns an Element object representing the element whose id property matches the specified string. Since element IDs are required to be unique if specified, they're a useful way to get access to a specific element quickly.getElementById("app")!, // default: document.body
MountOptions.lazy?: boolean | undefinedlazy: true, // mount on visibility
},
);
| Option | Type | Default | Description |
|---|---|---|---|
root |
Element |
document.body |
Scope discovery to a subtree |
lazy |
boolean |
false |
Use IntersectionObserver to mount on visibility |
Unmounting
mount() returns an object with an unmount function that tears down all discovered islands at once:
import { const ilha: IlhaFactoryilha, const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<unknown>Counter = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>counter</p>");
const { const unmount: () => void | Promise<void>unmount } = function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<unknown>Counter });
// Later — stops all effects, removes all listeners
const unmount: () => void | Promise<void>unmount();
Lazy mounting
When lazy: true is set, islands are not mounted immediately. Instead, each host element is observed with an IntersectionObserver and mounted only when it enters the viewport. This keeps the initial page load lean when islands are below the fold.
import { const ilha: IlhaFactoryilha, const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const HeavyChart: Island<unknown>HeavyChart = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<canvas></canvas>");
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type HeavyChart: Island<unknown>HeavyChart }, { MountOptions.lazy?: boolean | undefinedlazy: true });
Once an island becomes visible it mounts normally and is no longer observed.
Passing props
Props can be embedded directly in the HTML using data-ilha-props. mount() reads and parses this attribute automatically — no need to pass props through JavaScript:
<div data-ilha="Counter" data-ilha-props='{"start":10}'></div>
import { const ilha: IlhaFactoryilha, const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<unknown>Counter = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>counter</p>");
// No props needed here — they are read from data-ilha-props
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<unknown>Counter });
Hydration with state snapshots
When using .hydratable() on the server, the rendered HTML includes a data-ilha-state attribute with a snapshot of signal values. mount() reads this automatically and restores state without re-fetching or re-computing:
<div data-ilha="Counter" data-ilha-state='{"count":42}'></div>
import { const ilha: IlhaFactoryilha, const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<unknown>Counter = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>counter</p>");
// Reads data-ilha-state and restores signals from snapshot
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<unknown>Counter });
See Render and hydrate for how to generate this output on the server.
Scoping to a subtree
Pass a root element to limit discovery to a specific part of the page. This is useful when islands are injected dynamically into a container:
import { const ilha: IlhaFactoryilha, const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Widget: Island<unknown>Widget = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => "<p>widget</p>");
const const container: HTMLElementcontainer = var document: Document**`window.document`** returns a reference to the document contained in the window.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)document.Document.getElementById(elementId: string): HTMLElement | nullThe **`getElementById()`** method of the Document interface returns an Element object representing the element whose id property matches the specified string. Since element IDs are required to be unique if specified, they're a useful way to get access to a specific element quickly.getElementById("dynamic-content")!;
const { const unmount: () => void | Promise<void>unmount } = function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Widget: Island<unknown>Widget }, { MountOptions.root?: Element | undefinedroot: const container: HTMLElementcontainer });
Notes
- If a
data-ilhavalue has no matching key in the registry, that element is silently skipped. - In dev mode, double-mounting the same element logs a warning and returns a no-op for that element.
mount()is safe to call before the DOM is fully loaded if you wrap it in aDOMContentLoadedlistener or place the script at the end of<body>.
Build HTML strings
An XSS-safe tagged template for building HTML strings. This is ilha’s low-level templating API: you can use it directly, but JSX is the preferred authoring style for most apps.
Because html`` is plain TypeScript/JavaScript, it does not require JSX syntax, a JSX runtime import, or a build transform. That makes it a great fit for no-build apps, small scripts, server-only rendering, or any place where you want ilha’s escaping and composition rules without JSX tooling.
Interpolated values are HTML-escaped by default, making the safe path the default and explicit opt-in required for raw markup.
Basic usage
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const name: "<script>alert(1)</script>"name = "<script>alert(1)</script>";
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<p>${const name: "<script>alert(1)</script>"name}</p>`;
// → <p><script>alert(1)</script></p>
Interpolation rules
| Value type | Behavior |
|---|---|
string / number |
HTML-escaped |
null / undefined |
Omitted — renders as empty string |
raw(str) |
Inserted as-is, no escaping |
html\…`` |
Inserted as-is, already safe |
| Signal accessor | Called automatically, value is escaped |
| Array | Each item processed recursively, no commas |
Escaping
All string and number interpolations are escaped automatically:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const userInput: "<img src=x onerror=\"alert(1)\">"userInput = `<img src=x onerror="alert(1)">`;
const const count: 42count = 42;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<p>${const userInput: "<img src=x onerror=\"alert(1)\">"userInput}</p>`; // → <p><img src=x…></p>
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<p>${const count: 42count}</p>`; // → <p>42</p>
The characters &, <, >, ", and ' are all escaped.
Skipping null and undefined
null and undefined are silently omitted, making conditional rendering clean:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const error: nullerror = null;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<div>${const error: nullerror}</div>`;
// → <div></div>
Trusted markup with raw()
When you need to inject pre-sanitized or server-controlled markup, use raw() to opt out of escaping:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml, const raw: (value: string) => RawHtmlraw } from "ilha";
const const icon: "<svg aria-hidden=\"true\">…</svg>"icon = `<svg aria-hidden="true">…</svg>`;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<button>${function raw(value: string): RawHtmlraw(const icon: "<svg aria-hidden=\"true\">…</svg>"icon)} Submit</button>`;
// → <button><svg aria-hidden="true">…</svg> Submit</button>
Only use raw() with markup you fully control. Never pass user input to raw().
Nesting html results
Results of html are already safe and pass through unescaped when interpolated into a parent template. This is the foundation of composable templates:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const badge: RawHtmlbadge = const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<span class="badge">New</span>`;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<div class="card">
${const badge: RawHtmlbadge}
<p>Content</p>
</div>`;
// → <div class="card"><span class="badge">New</span><p>Content</p></div>
Signal accessors
Signal accessors can be interpolated without calling them. ilha detects signal accessors and calls them automatically, then escapes the result:
import { const ilha: IlhaFactoryilha, const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state } from "ilha";
const const Island: Island<unknown>Island = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const label: StateAccessor<string>label = state<string>(init?: string | (() => string) | undefined): StateAccessor<string>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state("<b>hello</b>");
return const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml` <p>${const label: StateAccessor<string>label}</p> `;
});
Both forms are equivalent. The no-call shorthand is purely a convenience.
URL attributes
Full-value URL attributes get the same scheme filter as JSX. If the interpolated value is the entire attribute value, an unsafe scheme (javascript:, vbscript:, script-capable data: subtypes such as data:text/html or data:image/svg+xml) drops the attribute:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const url: "javascript:alert(1)"url = "javascript:alert(1)";
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<a href=${const url: "javascript:alert(1)"url}>x</a>`;
// → <a>x</a> (attribute dropped, like JSX)
Safe targets pass through unchanged: https:/http: URLs, relative paths, mailto:, and image data: URIs like data:image/png;base64,…. srcdoc is dropped outright, matching JSX — attribute escaping cannot neutralize it because the browser re-decodes entities into live markup.
raw() in a URL attribute passes through unescaped, as in JSX — it is the author-owned escape hatch for values the filter rejects (for example a vetted SVG data URI). This only works when the interpolation is the whole value; partial values (href="/u/${id}") are plain escaped interpolation and cannot be scheme-checked.
Unquoted attribute values
An unquoted attribute whose value contains a character the HTML tokenizer treats as a terminator (whitespace, =, backtick) is re-emitted quoted and escaped. What used to be an attribute-injection hole is now one parsed attribute:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const value: "x onmouseover=alert(1) y"value = "x onmouseover=alert(1) y";
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<a href=${const value: "x onmouseover=alert(1) y"value}>x</a>`;
// → <a href="x onmouseover=alert(1) y">x</a> (no onmouseover attribute)
Benign unquoted values keep the author’s literal form (<a href=/path> stays <a href=/path>).
Script and style data
HTML escaping is the wrong encoding inside <script> and <style> content: entities are not decoded by the JS/CSS tokenizer, and a stray </script> or </style> truncates the element. Use the json() and css() helpers for executable-element data — they escape < so the value can never close the block or open the <!-- escape hatch:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml, function css(value: string): RawHtmlEscape text for a `<style>` element body. `<` becomes the CSS hex escape
`\3C `, so the value can never close the style block or open the `<!--`
escape hatch while surviving CSS parsing:
```ts
html`<style>${css(cssSource)}</style>`
```
Like `raw()`, `css()` is for element *content* only — interpolating it into
an attribute is author error.css, function json(value: unknown): RawHtmlSerialize a JSON-safe value into executable `<script>` content safely. `<`
is escaped as `\u003C`, so a value can never close the script block or open
the `<!--` escape hatch. Use it for script-element data:
```ts
html`<script>const d = ${json(payload)};</script>`
```
Like `raw()`, `json()` is for element *content* only — interpolating it into
an attribute is author error. It does not protect against values that are
not valid JSON (functions, symbols, undefined, cyclic objects are dropped
or throw, matching `JSON.stringify`).json } from "ilha";
const const payload: {
name: string;
}
payload = { name: stringname: "</script><img src=x>" };
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<script>
const d = ${function json(value: unknown): RawHtmlSerialize a JSON-safe value into executable `<script>` content safely. `<`
is escaped as `\u003C`, so a value can never close the script block or open
the `<!--` escape hatch. Use it for script-element data:
```ts
html`<script>const d = ${json(payload)};</script>`
```
Like `raw()`, `json()` is for element *content* only — interpolating it into
an attribute is author error. It does not protect against values that are
not valid JSON (functions, symbols, undefined, cyclic objects are dropped
or throw, matching `JSON.stringify`).json(const payload: {
name: string;
}
payload)};
</script>`;
// → <script>const d = {"name":"\u003C/script>\u003Cimg src=x"};</script>
const const cssSource: "a{content:'</style>'}"cssSource = "a{content:'</style>'}";
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<style>
${function css(value: string): RawHtmlEscape text for a `<style>` element body. `<` becomes the CSS hex escape
`\3C `, so the value can never close the style block or open the `<!--`
escape hatch while surviving CSS parsing:
```ts
html`<style>${css(cssSource)}</style>`
```
Like `raw()`, `css()` is for element *content* only — interpolating it into
an attribute is author error.css(const cssSource: "a{content:'</style>'}"cssSource)}
</style>`;
// → <style>a{content:'\3C /style>'}</style>
json() takes any JSON.stringify-able value and escapes < as \u003C; the JS engine decodes it inside string literals, so data round-trips exactly. css() takes a string and escapes < as the CSS hex escape \3C. Like raw(), both are for element content only — interpolating them into an attribute is author error.
Native event handlers
Use lowercase on* attributes with function interpolations inside an island render:
import { const ilha: IlhaFactoryilha, const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state } from "ilha";
const const Form: Island<unknown>Form = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const name: StateAccessor<string>name = state<string>(init?: string | (() => string) | undefined): StateAccessor<string>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state("");
return const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`
<form
onsubmit=${(event: SubmitEventevent: SubmitEvent) => {
event: SubmitEventevent.Event.preventDefault(): voidThe **`preventDefault()`** method of the Event interface tells the user agent that the event is being explicitly handled, so its default action, such as page scrolling, link navigation, or pasting text, should not be taken.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)preventDefault();
var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(const name: MarkedSignalAccessor
() => string (+1 overload)
name());
}}
>
<input
bind:value=${const name: StateAccessor<string>name}
onselect=${(event: Eventevent: Event) =>
var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(event: Eventevent.Event.currentTarget: EventTarget | nullThe **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)currentTarget)}
/>
<button type="submit">Save</button>
</form>
`;
});
The same syntax works with onclick, oninput, onchange, onreset, and other native event names. The handler receives the native DOM event and a second context argument containing a lifecycle signal.
Add one listener modifier after the event name:
import { const ilha: IlhaFactoryilha, const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml, type NativeEventContext } from "ilha";
const const Search: Island<unknown>Search = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(
() => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`
<input
oninput:abortable=${async (
event: InputEventevent: InputEvent,
{ signal: AbortSignalsignal }: NativeEventContext,
) => {
const const input: HTMLInputElementinput = event: InputEventevent.Event.currentTarget: EventTarget | nullThe **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)currentTarget as HTMLInputElement;
await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch(`/api/search?q=${const input: HTMLInputElementinput.HTMLInputElement.value: stringThe **`value`** property of the HTMLInputElement interface represents the current value of the <input> element as a string.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/value)value}`, { RequestInit.signal?: AbortSignal | null | undefinedAn AbortSignal to set request's signal.signal });
}}
/>
`,
);
Use :once, :capture, :passive, or :abortable. The :abortable modifier aborts the previous invocation when the same event fires again. Native handlers accept one modifier.
Event handlers work only while an island renders and mounts the template. Ilha refreshes closures after re-renders and aborts the handler signal when it replaces a listener or starts unmounting the island. A standalone html`` result has no host or lifecycle, so Ilha omits its event function.
Functions never become inline HTML attributes during SSR. Ilha emits inert markup and attaches each function with addEventListener during mount.
Keep the function as an event-attribute interpolation. You can compose an attribute-only nested template when the attribute is conditional:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const enabled: trueenabled = true;
const const handler: () => voidhandler = () => {};
const const events: RawHtml | ""events = const enabled: trueenabled ? const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml` onclick=${const handler: () => voidhandler}` : "";
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<button class="save" ${const events: RawHtml | ""events}>Save</button>`;
Ilha cannot register handlers hidden inside serialized attribute objects or raw() strings. Those paths produce plain markup. For host-level listeners or delegated selectors, attach them with effect.once() using native DOM APIs.
Use bind:* for signal synchronization. Use effect.once() to attach host listeners with native DOM APIs when handlers do not sit on one rendered element.
List rendering
Arrays are processed recursively with no comma joining. The canonical list pattern is:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const fruits: string[]fruits = ["apple", "banana", "cherry"];
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`
<ul>
${const fruits: string[]fruits.Array<string>.map<RawHtml>(callbackfn: (value: string, index: number, array: string[]) => RawHtml, thisArg?: any): RawHtml[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((fruit: stringfruit) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<li>${fruit: stringfruit}</li>`)}
</ul>
`;
// → <ul><li>apple</li><li>banana</li><li>cherry</li></ul>
Each html result in the array passes through unescaped. Mixed arrays of strings and html results also work — each item is processed by its own rules.
Whitespace and indentation
html\`` automatically strips leading and trailing blank lines and dedents the template based on the minimum indentation found. This keeps rendered output clean regardless of how the template is indented in source:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const result: RawHtmlresult = const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`
<div>
<p>Hello</p>
</div>
`;
// → <div>\n <p>Hello</p>\n</div>
Return type
html returns a RawHtml object, not a plain string. This lets ilha distinguish between trusted and untrusted content when the result is interpolated into another template. To get the plain string value, access .value or let ilha unwrap it at a render boundary:
import { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const result: RawHtmlresult = const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml`<p>hello</p>`;
const result: RawHtmlresult.RawHtml.value: stringvalue; // → "<p>hello</p>"
In practice you rarely need to access .value directly — ilha handles unwrapping automatically at render time.
Notes
- html is purely a runtime helper with no compiler step. It works in any JavaScript environment including Node, Bun, Deno, and the browser.
- Full-value attribute interpolation (
href=${…}/href="${…}") applies the same URL/style policy as JSX. Values assembled from several interpolations (href="/u/${a}/${b}") stay plain escaped interpolation — scheme-checking cannot span partial values, so keep user input out of URL pieces. - Style objects (
style=${{ color: "red" }}) serialize through the same allowlisted property list as JSX. - For stylesheets and script bodies, use
json()/css()(see above) rather than raw string interpolation.
Render trusted markup
Marks a string as trusted HTML, bypassing escaping when rendered in JSX or interpolated inside html. Use it when you need to inject markup you fully control — icons, pre-rendered fragments, or server-sanitized content.
Basic usage
import { const raw: (value: string) => RawHtmlraw } from "ilha";
<"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>{function raw(value: string): RawHtmlraw("<em>hello</em>")}</"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>;
// → <div><em>hello</em></div>
Without raw(), the same string would be escaped:
<"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>{"<em>hello</em>"}</"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>
// → <div><em>hello</em></div>
When to use it
raw() is appropriate when the markup comes from a source you fully control:
import { const ilha: IlhaFactoryilha, const raw: (value: string) => RawHtmlraw } from "ilha";
// SVG icons defined in your codebase
const const chevron: "<svg viewBox=\"0 0 16 16\">\n <path d=\"M4 6l4 4 4-4\"/>\n</svg>"chevron = `<svg viewBox="0 0 16 16">
<path d="M4 6l4 4 4-4"/>
</svg>`;
const const Dropdown: Island<unknown>Dropdown = ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => (
<"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
button>Options {function raw(value: string): RawHtmlraw(const chevron: "<svg viewBox=\"0 0 16 16\">\n <path d=\"M4 6l4 4 4-4\"/>\n</svg>"chevron)}</"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
button>
));
import { const raw: (value: string) => RawHtmlraw } from "ilha";
// Pre-rendered HTML from a trusted server-side renderer
const const renderedMarkdown: "<h1>Title</h1><p>Body text.</p>"renderedMarkdown = `<h1>Title</h1><p>Body text.</p>`;
<"article": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLElement>>article>{function raw(value: string): RawHtmlraw(const renderedMarkdown: "<h1>Title</h1><p>Body text.</p>"renderedMarkdown)}</"article": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLElement>>article>;
When not to use it
Never pass user input to raw(). It disables all escaping, so any unescaped string becomes a potential XSS vector:
import { const raw: (value: string) => RawHtmlraw } from "ilha";
// ❌ Never do this
const const userComment: "<script>alert(1)</script>"userComment = `<script>alert(1)</script>`;
<"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{function raw(value: string): RawHtmlraw(const userComment: "<script>alert(1)</script>"userComment)}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>;
// ✅ Do this instead — JSX escapes it automatically
<"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{const userComment: "<script>alert(1)</script>"userComment}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>;
Composing with JSX
JSX results are already treated as safe and pass through unescaped without needing raw(). Reserve raw() for plain strings that contain trusted markup:
import { const raw: (value: string) => RawHtmlraw } from "ilha";
// JSX result — no raw() needed
const const badge: JSX.Elementbadge = <"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span class?: RawHtml | ClassValue | undefinedclass="badge">New</"span": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLSpanElement>>span>;
<"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>{const badge: JSX.Elementbadge}</"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>;
// Plain string with markup — raw() required
const const iconStr: "<svg>…</svg>"iconStr = `<svg>…</svg>`;
<"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>{function raw(value: string): RawHtmlraw(const iconStr: "<svg>…</svg>"iconStr)}</"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>;
Return type
raw() returns a RawHtml object. This means raw values compose freely with JSX and arrays:
import { const raw: (value: string) => RawHtmlraw } from "ilha";
const const icons: string[]icons = ["<svg>…</svg>", "<svg>…</svg>"];
<"ul": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLUListElement>>ul>
{const icons: string[]icons.Array<string>.map<JSX.Element>(callbackfn: (value: string, index: number, array: string[]) => JSX.Element, thisArg?: any): JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((icon: stringicon) => (
<"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>{function raw(value: string): RawHtmlraw(icon: stringicon)}</"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>
))}
</"ul": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLUListElement>>ul>;
Notes
raw()only has an effect when rendered by ilha JSX orhtml. Elsewhere it simply wraps the string in aRawHtmlobject with no other transformation.- There is no runtime sanitization inside
raw(). If you need to accept user-generated HTML, sanitize it with a dedicated library such as DOMPurify before passing it toraw().