Babel plugin to compile JSX to vanilla JavaScript with Flick reactivity.
bun add -d @flickjs/compiler @babel/core
# or
npm install --save-dev @flickjs/compiler @babel/coreThe Flick compiler transforms JSX into vanilla JavaScript DOM operations with fine-grained reactive bindings. Unlike React's JSX, which creates a virtual DOM, Flick's JSX compiles directly to imperative DOM manipulation code that updates only when signals change.
Create a babel.config.js file:
export default {
plugins: ['@flickjs/compiler']
};Or in CommonJS format:
module.exports = {
plugins: ['@flickjs/compiler']
};Bun has built-in Babel support. Just add the babel config file and build:
bun build src/main.tsx --outdir distInstall the Babel plugin for Vite:
bun add -d vite-plugin-babelThen in vite.config.js:
import { defineConfig } from 'vite';
import babel from 'vite-plugin-babel';
export default defineConfig({
plugins: [
babel({
babelConfig: {
plugins: ['@flickjs/compiler']
}
})
]
});Install babel-loader:
npm install --save-dev babel-loaderThen in webpack.config.js:
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@flickjs/compiler']
}
}
}
]
}
};The compiler transforms JSX elements into vanilla DOM operations and wraps reactive expressions in effect() calls.
Input:
<div class="container">
<h1>Hello World</h1>
</div>Output:
(() => {
const el = document.createElement('div');
el.className = 'container';
const child = document.createElement('h1');
child.textContent = 'Hello World';
el.append(child);
return el;
})();Input:
<h1>Count: {count()}</h1>Output:
(() => {
const el = document.createElement('h1');
const text1 = document.createTextNode('Count: ');
const text2 = document.createTextNode('');
el.append(text1, text2);
effect(() => {
text2.data = count();
});
return el;
})();Input:
<button onclick={() => count.set(count() + 1)}>Click me</button>Output:
(() => {
const el = document.createElement('button');
el.onclick = () => count.set(count() + 1);
el.textContent = 'Click me';
return el;
})();Input:
<input value={name()} oninput={(e) => name.set(e.target.value)} />Output:
(() => {
const el = document.createElement('input');
effect(() => {
el.value = name();
});
el.oninput = (e) => name.set(e.target.value);
return el;
})();Input:
function Button({ label }) {
return <button>{label}</button>;
}
function App() {
return <Button label="Click" />;
}Output:
function Button({ label }) {
const el = document.createElement('button');
el.textContent = label;
return el;
}
function App() {
return Button({ label: 'Click' });
}To use JSX with TypeScript, add to your tsconfig.json:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@flickjs/runtime"
}
}Or use the classic JSX transform:
{
"compilerOptions": {
"jsx": "preserve"
}
}- ✅ Elements:
<div>,<span>, etc. - ✅ Attributes:
class,id,style, etc. - ✅ Event handlers:
onclick,oninput, etc. - ✅ Children: text, elements, arrays
- ✅ Components: function components
- ✅ Reactive expressions:
{count()} - ✅ Fragments:
<>...</>
- ❌ Class components (use function components)
- ❌ Lifecycle methods (use effects)
- ❌ Context API (pass props)
- ❌ Refs (use direct DOM manipulation)
Since JSX compiles to functions, you can use regular JavaScript for conditionals:
function Greeting({ isLoggedIn }) {
return <div>{isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please sign in</h1>}</div>;
}Or with signals:
function Greeting() {
const isLoggedIn = signal(false);
return <div>{isLoggedIn() ? <h1>Welcome back!</h1> : <h1>Please sign in</h1>}</div>;
}Use .map() to render lists:
function TodoList() {
const todos = signal([
{ id: 1, text: 'Learn Flick' },
{ id: 2, text: 'Build app' }
]);
return (
<ul>
{todos().map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}Use fragments to return multiple elements:
function Header() {
return (
<>
<h1>Title</h1>
<p>Subtitle</p>
</>
);
}You can pass style objects directly:
function StyledDiv() {
const color = signal('red');
return <div style={{ color: color(), fontSize: '16px' }}>Styled text</div>;
}The compiler generates optimal code:
- Zero runtime overhead for static content
- Minimal reactivity cost - only reactive expressions are tracked
- No virtual DOM - direct DOM manipulation
- Tree-shakeable - unused code is eliminated
When building with source maps, you can debug the original JSX code:
bun build src/main.tsx --outdir dist --sourcemapTo see the compiled output, use Babel CLI:
npx babel src/main.tsx --plugins @flickjs/compilerfunction LoginForm() {
const email = signal('');
const password = signal('');
const handleSubmit = (e) => {
e.preventDefault();
console.log({ email: email(), password: password() });
};
return (
<form onsubmit={handleSubmit}>
<input
type="email"
value={email()}
oninput={(e) => email.set(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password()}
oninput={(e) => password.set(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}function Button({ active }) {
const isActive = signal(active);
return (
<button class={isActive() ? 'btn btn-active' : 'btn'} onclick={() => isActive.set(!isActive())}>
Toggle
</button>
);
}function Card({ title, children }) {
return (
<div class="card">
<h2>{title}</h2>
<div class="card-body">{children}</div>
</div>
);
}
function App() {
return (
<Card title="Hello">
<p>This is the card content</p>
</Card>
);
}If you're coming from React:
- Components: Use function components (same as React)
- State: Replace
useStatewithsignal - Effects: Replace
useEffectwitheffect - Props: Works the same way
- Events: Use lowercase names (
onclicknotonClick) - Class: Use
classnotclassName - Style: Can use objects (same as React)
React:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Flick:
function Counter() {
const count = signal(0);
return <button onclick={() => count.set(count() + 1)}>{count()}</button>;
}MIT © Jay Malave