CSS

Attaches scoped styles to the island. Styles are automatically wrapped in a @scope rule bounded to the island host, so they apply only within the island and do not leak into child islands.

Basic usage

The preview runs the same .css tagged-template form as in the snippet below; styles stay scoped to the island host.

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, 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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha"; const const Card: Island<Record<string, unknown>, Record<never, never>>Card =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, 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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>css`
.card__title { font-weight: 700; } .card__button { background: teal; color: white; } `.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => ( <IntrinsicElements[string]: anydiv class?: string | unknown[] | Record<string, boolean> | undefinedclass="card"> <IntrinsicElements[string]: anyp class?: string | unknown[] | Record<string, boolean> | undefinedclass="card__title">HelloIntrinsicElements[string]: anyp> <IntrinsicElements[string]: anybutton type: stringtype="button" class?: string | unknown[] | Record<string, boolean> | undefinedclass="card__button"> Click me IntrinsicElements[string]: anybutton> IntrinsicElements[string]: anydiv> ));

Plain string form

.css() also accepts a plain string, which is useful when importing styles from an external file:

import ilha from "ilha";
import styles from "./card.css?raw";

const Card = ilha.css(styles).render(() => (
  <div class="card">
    <p class="card__title">p>
  div>
));

Interpolations

When using the tagged template form, interpolations work as normal string concatenation:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, 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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha"; const const accent: "coral"accent = "coral"; const const Button: Island<Record<string, unknown>, Record<never, never>>Button =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, 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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>css`
.btn { color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.375rem; } .btn--accent { background: ${const accent: "coral"accent}; } `.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => ( <IntrinsicElements[string]: anybutton type: stringtype="button" class?: string | unknown[] | Record<string, boolean> | undefinedclass="btn btn--accent"> Go IntrinsicElements[string]: anybutton> ));

Using the css tagged template

ilha ships a named css export that works as a passthrough tag for editor tooling. It enables LSP syntax highlighting and Prettier formatting for CSS strings without any runtime transformation. Use it to author styles outside the builder chain and pass the result in:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, 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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha, { const css: (strings: TemplateStringsArray | string, ...values: (string | number)[]) => stringcss } from "ilha"; const const styles: stringstyles = const css: (strings: TemplateStringsArray | string, ...values: (string | number)[]) => stringcss` .card__title { font-weight: 700; } .card__action { background: teal; color: white; } `; const const Card: Island<Record<string, unknown>, Record<never, never>>Card =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, 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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>css(const styles: stringstyles).IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => (
<IntrinsicElements[string]: anydiv class?: string | unknown[] | Record<string, boolean> | undefinedclass="card"> <IntrinsicElements[string]: anyp class?: string | unknown[] | Record<string, boolean> | undefinedclass="card__title">TitleIntrinsicElements[string]: anyp> <IntrinsicElements[string]: anybutton type: stringtype="button" class?: string | unknown[] | Record<string, boolean> | undefinedclass="card__action"> Action IntrinsicElements[string]: anybutton> IntrinsicElements[string]: anydiv> ));

css (named export) is a plain passthrough tag for tooling. .css() (builder method) is what actually attaches styles to the island. They are intentionally separate.

How scoping works

ilha wraps your styles in a @scope rule that constrains them to the island host and punches a hole at any nested [data-ilha] element:

@scope (:scope) to ([data-ilha]) {
  .card__title {
    font-weight: 700;
  }
  .card__button {
    background: teal;
    color: white;
  }
}

This means:

  • Styles apply to descendants of the island host.
  • Styles do not leak into child islands nested inside.
  • Selectors use low specificity and do not win unnecessary cascade wars with utility classes.

SSR output

During SSR, a

Hello

Client mount

On the client, the style element is injected once as the first child of the host. It is preserved across re-renders — morph never replaces it. During hydration, the SSR-emitted
node is reused and not duplicated.

Notes

  • Calling .css() more than once on the same builder chain is not supported. In dev mode a warning is logged and only the last stylesheet is used. Compose all styles into a single .css() call.
  • .css() is compatible with .hydratable() — the style tag is included inside the data-ilha wrapper regardless of the snapshot option.
  • Browser support for @scope is required. Check caniuse.com/css-cascade-scope for current coverage if you need to support older browsers.
<