HTML

An XSS-safe tagged template for building HTML strings. 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[]) => RawHtml
html
} from "ilha";
const
const name: "<script>alert(1)</script>"
name
= "<script>alert(1)</script>";
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<p>${
const name: "<script>alert(1)</script>"
name
}</p>`;
// → <p>&lt;script&gt;alert(1)&lt;/script&gt;</p>

Interpolation rules

Value typeBehavior
string / numberHTML-escaped
null / undefinedOmitted — renders as empty string
raw(str)Inserted as-is, no escaping
html\…``Inserted as-is, already safe
Signal accessorCalled automatically, value is escaped
ArrayEach item processed recursively, no commas

Escaping

All string and number interpolations are escaped automatically:

import { 
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
} from "ilha";
const
const userInput: "<img src=x onerror=\"alert(1)\">"
userInput
= `<img src=x onerror="alert(1)">`;
const
const count: 42
count
= 42;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<p>${
const userInput: "<img src=x onerror=\"alert(1)\">"
userInput
}</p>`; // → <p>&lt;img src=x…&gt;</p>
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<p>${
const count: 42
count
}</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[]) => RawHtml
html
} from "ilha";
const
const error: null
error
= null;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<div>${
const error: null
error
}</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[]) => RawHtml
html
,
const raw: (value: string) => RawHtml
raw
} from "ilha";
const
const icon: "<svg aria-hidden=\"true\">…</svg>"
icon
= `<svg aria-hidden="true">…</svg>`;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<button>${
function raw(value: string): RawHtml
raw
(
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[]) => RawHtml
html
} from "ilha";
const
const badge: RawHtml
badge
=
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<span class="badge">New</span>`;
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<div class="card">
${
const badge: RawHtml
badge
}
<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: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
, {
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
} from "ilha";
const
const Island: Island<Record<string, unknown>, MergeState<Record<string, never>, "label", string>>
Island
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<string, "label">(key: "label", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "label", string>, Record<string, never>>
state
("label", "<b>hello</b>")
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "label", string>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "label", string>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "label", string>>
render
(({
state: IslandState<MergeState<Record<string, never>, "label", string>>
state
}) =>
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
` <p>${
state: IslandState<MergeState<Record<string, never>, "label", string>>
state
.
label: SignalAccessor<string>
label
}</p> `);

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

List rendering

Arrays are processed recursively with no comma joining. The canonical list pattern is:

import { 
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
} from "ilha";
const
const fruits: string[]
fruits
= ["apple", "banana", "cherry"];
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`
<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: string
fruit
) =>
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<li>${
fruit: string
fruit
}</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[]) => RawHtml
html
} from "ilha";
const
const result: RawHtml
result
=
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`
<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[]) => RawHtml
html
} from "ilha";
const
const result: RawHtml
result
=
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`<p>hello</p>`;
const result: RawHtml
result
.
RawHtml.value: string
value
; // → "<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.