Gracile — Babel JSX to Literals (Vite plugin)
A Babel JSX to Literals convenience wrapper for Vite
projects.
Compiles JSX/TSX to html tagged template literals at build time, using
Babel and
@gracile-labs/babel-plugin-jsx-to-literals.
Tip
You can use this plugin with any Vite+Lit setup!
It’s totally decoupled from the Gracile framework.
Note
This is the Babel-based approach. For a newer, native TS compiler plugin alternative, see @gracile-labs/vite-plugin-jsx-forge (based on ts-patch).
Installation
npm i @gracile-labs/vite-plugin-babel-jsx-to-literals @gracile-labs/babel-plugin-jsx-to-literals
Setup
Vite config
📄 /vite.config.ts
import { gracileJsximport gracileJsx } from '@gracile-labs/vite-plugin-babel-jsx-to-literals/vite';
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.
: [
gracileJsximport gracileJsx (),
// ...
],
});
You can pass custom options to the underlying Babel plugin:
gracileJsx({
babelPlugin: {
autoImports: true,
// Override import mappings, etc.
},
});
TypeScript config
📄 /tsconfig.json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@gracile-labs/babel-plugin-jsx-to-literals",
},
}
Ambient types
Add the JSX types reference in a .d.ts file (or use a
triple-slash directive
at the top of your entry files):
📄 /src/ambient.d.ts
/// <reference types="@gracile-labs/babel-plugin-jsx-to-literals/jsx-runtime" />
Or import the ambient types from this package directly:
/// <reference types="@gracile-labs/vite-plugin-babel-jsx-to-literals/ambient" />
Usage
JSX is compiled statically to Lit html tagged templates via Babel. This is
not a JSX runtime — think Solid-style compilation, but targeting Lit (or any
compatible tagged template library).
Basic example
📄 /src/my-template.tsx
const nameconst name: "World" = 'World';
const elconst el: React.JSX.Element = <spanReact.JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> classclass: string ="greeting">{nameconst name: "World" }</spanReact.JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> >;
Compiles to:
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';
const nameconst name: "World" = 'World';
const elconst el: 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.
`<span class="greeting">${nameconst name: "World" }</span>`;
Attribute bindings
<input
class="toggle"
type="checkbox"
prop:checked={todo.completed}
on:input={(e) => store.toggle(todo.id, e.currentTarget.checked)}
/>
Compiles to:
html`<input
class="toggle"
type="checkbox"
.checked=${todo.completed}
@input=${(ee: any ) => store.toggle(todo.id, ee: any .currentTarget.checked)}
/>`;
| Prefix | Output | Use case |
|---|---|---|
prop: | .property=${…} | Lit property binding |
on: | @event=${…} | DOM / custom event |
bool: | ?attr=${…} | Lit boolean attribute |
attr: | attr=${JSON.stringify(…)} | Serialized attribute (SSR) |
if: | attr=${ifDefined(…)} | Conditional attribute |
class:map | class=${classMap(…)} | Class map directive |
class:list | class=${clsx(…)} | Class list (via clsx) |
style:map | style=${styleMap(…)} | Style map directive |
Control helpers
import { For } from '@gracile-labs/vite-plugin-babel-jsx-to-literals/components/for';
import { Show } from '@gracile-labs/vite-plugin-babel-jsx-to-literals/components/show';
const el = (
<ul>
<For each={items} key={({ id }) => id}>
{(item) => <li>{item.name}</li>}
</For>
</ul>
);
const header = (
<Show when={isLoggedIn}>
<span>Welcome back!</span>
</Show>
);
Template flavor directives
Place a "use html-*" directive at the top of a file to control which html
tag function is imported:
| Directive | Import source | Use case |
|---|---|---|
| (default) | lit | Standard client templates |
'use html-server' | @gracile/gracile/server-html | Server-side rendering |
'use html-signal' | @lit-labs/signals | Signal-aware templates |
Auto-imports
All runtime imports (html, ref, classMap, styleMap, repeat,
ifDefined, unsafeHTML, etc.) are injected automatically based on usage —
you never need to import directives manually.
See the full babel-plugin-jsx-to-literals documentation for the complete syntax reference (bindings, components, control helpers, etc.).