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 { gracileimport gracile } 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 (),
gracileimport gracile ({
/* ... */
}),
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 { defineRouteimport defineRoute } 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 defineRouteimport defineRoute ({
documentdocument: (context: any) => any : (contextcontext: any ) => documentimport document (contextcontext: any ),
templatetemplate: () => TemplateResult<1> : () => 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.