Gracile — (P)React, Solid, Svelte, Vue Islands
Gracile Islands let you server-render and hydrate components from any UI
framework — React, Vue, Svelte, Solid, Preact — inside your Lit SSR pages using
the <is-land> custom element.
Caution
Experimental. This add-on is under active development and its API may change.
Concept
The Islands pattern lets you embed interactive “islands” of rich UI inside an otherwise statically rendered page. Each island is independently server-rendered during SSR and selectively hydrated on the client.
Gracile Islands builds on top of the Lit SSR
ElementRenderer API, registering a
custom renderer for the <is-land> tag that delegates rendering to
framework-specific adapters.
Installation
npm i @gracile-labs/islands
You also need the Vite plugin(s) for any framework you want to use. For example, to use React and Vue islands:
npm i @vitejs/plugin-react @vitejs/plugin-vue react react-dom vue
Setup
1. Add the Vite plugin
In your vite.config.ts, add gracileIslands() after the main gracile()
plugin and any framework plugins:
📄 ./vite.config.ts
import { gracileconst gracile: (config?: GracileConfig) => any[]The main Vite plugin for loading the Gracile framework.
} from '@gracile/gracile/plugin';
import { gracileIslandsimport gracileIslands } from '@gracile-labs/islands';
import reactimport react from '@vitejs/plugin-react';
import vueimport vue from '@vitejs/plugin-vue';
import { defineConfigfunction defineConfig(config: UserConfig): UserConfig (+5 overloads)Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.
} from 'vite';
export default defineConfigfunction defineConfig(config: UserConfig): UserConfig (+5 overloads)Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.
({
pluginsUserConfig.plugins?: PluginOption[] | undefinedArray of vite plugins to use.
: [
reactimport react ({ includeinclude: string[] : ['**/*.react.{js,jsx,ts,tsx}'] }),
vueimport vue (),
gracilefunction gracile(config?: GracileConfig): any[]The main Vite plugin for loading the Gracile framework.
({
/* ... */
}),
gracileIslandsimport gracileIslands ({ debugdebug: boolean : true }),
],
});
2. Create an island registry
Create an islands.config.ts file at the root of your project. This file maps
island names to their framework components and tells the server how to render
them:
📄 ./islands.config.ts
import { defineReactIslandsimport defineReactIslands } from '@gracile-labs/islands/react/define';
import { defineVueIslandsimport defineVueIslands } from '@gracile-labs/islands/vue/define';
import MyReactFormimport MyReactForm from './src/components/my-form.react';
import MyVueWidgetimport MyVueWidget from './src/components/my-widget.vue';
export default {
...defineReactIslandsimport defineReactIslands ({ MyReactFormtype MyReactForm: any }),
...defineVueIslandsimport defineVueIslands ({ MyVueWidgettype MyVueWidget: any }),
};
The define*Islands helpers use conditional exports — the node (server)
export renders to a static HTML string, while the browser (client) export
hydrates the component in-place.
3. Add the client entry
📄 ./src/document.client.ts
import '@gracile-labs/islands/client';
Usage in routes
📄 ./src/routes/(home).ts
import { defineRoutefunction defineRoute<GetHandlerData extends HandlerDataHtml = undefined, PostHandlerData extends HandlerDataHtml = undefined, CatchAllHandlerData extends HandlerDataHtml = undefined, StaticPathOptions extends StaticPathOptionsGeneric | undefined = undefined, RouteContext extends RouteContextGeneric = {
url: URL;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions["props"] : GetHandlerData | PostHandlerData extends undefined ? CatchAllHandlerData extends Response ? never : CatchAllHandlerData : {
GET: GetHandlerData extends Response ? never : GetHandlerData;
POST: PostHandlerData extends Response ? never : PostHandlerData;
};
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions["params"] : Parameters;
}>(options: {
handler?: StaticPathOptions extends object ? never : Handler<CatchAllHandlerData> | {
GET?: Handler<GetHandlerData>;
POST?: Handler<PostHandlerData>;
QUERY?: Handler<Response>;
PUT?: Handler<Response>;
PATCH?: Handler<Response>;
DELETE?: Handler<Response>;
HEAD?: Handler<Response>;
OPTIONS?: Handler<Response>;
} | undefined;
staticPaths?: () => MaybePromise<StaticPathOptions[]> | undefined;
prerender?: boolean | undefined;
document?: DocumentTemplate<RouteContext> | undefined;
template?: BodyTemplate<RouteContext> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a file-based route for Gracile to consume.
Important: Property order matters for type inference.
handler (or staticPaths) must be declared before document and
template in the options object. TypeScript resolves generic type
parameters from object properties in declaration order — the handler's
return type feeds into RouteContext.props, which is then used to type
the context parameter of document and template.
If document/template appear first, props will be inferred as
undefined.
} from '@gracile/gracile/route';
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { documentimport document } from '../document.js';
export default defineRoutedefineRoute<undefined, undefined, undefined, undefined, {
url: URL;
props: undefined;
params: Parameters;
}>(options: {
handler?: Handler<undefined> | {
GET?: Handler<undefined> | undefined;
POST?: Handler<undefined> | undefined;
QUERY?: Handler<Response>;
PUT?: Handler<Response>;
PATCH?: Handler<Response>;
DELETE?: Handler<Response>;
HEAD?: Handler<Response>;
OPTIONS?: Handler<Response>;
} | undefined;
staticPaths?: (() => MaybePromise<...> | undefined) | undefined;
prerender?: boolean | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a file-based route for Gracile to consume.
Important: Property order matters for type inference.
handler (or staticPaths) must be declared before document and
template in the options object. TypeScript resolves generic type
parameters from object properties in declaration order — the handler's
return type feeds into RouteContext.props, which is then used to type
the context parameter of document and template.
If document/template appear first, props will be inferred as
undefined.
({
documentdocument?: DocumentTemplate<{
url: URL;
props: undefined;
params: Parameters;
}> | undefined
A function that returns a server only template.
Route context is provided at runtime during the build.
Receives the same RouteContext as template — including typed
props from handler. Declare handler above this property.
: (contextcontext: {
url: URL;
props: undefined;
params: Parameters;
}
) => documentimport document (contextcontext: {
url: URL;
props: undefined;
params: Parameters;
}
),
templatetemplate?: BodyTemplate<{
url: URL;
props: undefined;
params: Parameters;
}> | undefined
A function that returns a server only or a Lit client hydratable template.
Route context is provided at runtime during the build.
Receives the same RouteContext as document — including typed
props from handler. Declare handler above this property.
: () => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<h1>My Page</h1>
<is-land load="MyReactForm"></is-land>
<is-land load="MyVueWidget"></is-land>
`,
});
Passing props
html`
<is-land
load="MyReactForm"
props="${JSON.stringify({ title: 'Contact Us' })}"
></is-land>
`;
Light DOM rendering
html`<is-land load="MyVueWidget" light></is-land>`;
Supported frameworks
| Framework | Server define |
|---|---|
| React | @gracile-labs/islands/react/define |
| Vue | @gracile-labs/islands/vue/define |
| Svelte | @gracile-labs/islands/svelte/define |
| Solid | @gracile-labs/islands/solid/define |
| Preact | @gracile-labs/islands/preact/define |
[!TIP] When using Solid, you also need to include
generateHydrationScript()in your document<head>for hydration to work properly.
How it works
- The
gracileIslands()Vite plugin loadsislands.config.tsat server startup and registers a Lit SSRElementRendererfor the<is-land>tag. - During SSR, Lit encounters
<is-land>elements and delegates to theGenericIslandRenderer, which calls the framework’s server-side render function. - On the client, the
<is-land>custom element hydrates itself by looking up the component in the registry and calling the framework’s client-side hydrate function.