Skip to content

html

An XSS-safe tagged template for building HTML strings with automatic escaping and signal interpolation.

Updated View as Markdown

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>&lt;script&gt;alert(1)&lt;/script&gt;</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>&lt;img src=x…&gt;</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: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
, { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const Island: Island<RootInput, MergeState<RootState, "label", string>>Island =
const ilha: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<string, "label">(key: "label", init?: StateInit<RootInput, string> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "label", string>, RootDerived, RootActions>state("label", "<b>hello</b>") .IlhaBuilder<RootInput, MergeState<RootState, "label", string>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "label", string>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "label", string>>render(({ state: IslandState<MergeState<RootState, "label", string>>state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml` <p>${state: IslandState<MergeState<RootState, "label", string>>state.label: SignalAccessor<string>label}</p> `);

Both forms are equivalent. The no-call shorthand is purely a convenience.

Native event handlers

Use lowercase on* attributes with function interpolations inside an island render:

import 
const ilha: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
, { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml } from "ilha";
const const Form: Island<RootInput, MergeState<RootState, "name", string>>Form =
const ilha: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<string, "name">(key: "name", init?: StateInit<RootInput, string> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>state("name", "").IlhaBuilder<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "name", string>>render(
({ state: IslandState<MergeState<RootState, "name", string>>state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml` <form onsubmit=${(event: SubmitEventevent: SubmitEvent) => { event: SubmitEventevent.Event.preventDefault(): void
The **`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[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(state: IslandState<MergeState<RootState, "name", string>>state.
name: MarkedSignalAccessor
() => string (+1 overload)
name
());
}} > <input bind:value=${state: IslandState<MergeState<RootState, "name", string>>state.name: SignalAccessor<string>name} onselect=${(event: Eventevent: Event) => var console: Consoleconsole.Console.log(...data: any[]): void
The **`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 | null
The **`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: RootBuilder & DirectIslandFactory & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    ... 5 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
, { const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtmlhtml, type NativeEventContext } from "ilha";
const const Search: Island<RootInput, RootState>Search = ilha<RootInput>(fn: (ctx: RenderContext<RootInput, RootState, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, RootState>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 | null
The **`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: string
The **`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 | undefined
An 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.

Use bind:* for signal synchronization. Use .on() for selectors, host listeners, its full handler context, or combined modifiers.

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.
@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
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.
  • Do not use html for CSS or attribute values where HTML escaping is not appropriate. Use the css tag for stylesheets and plain template literals for everything else.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close