TSX in. Your stack out.
BarefootJS compiles TSX components into your backend's own templates — Go, Rails, Django, Perl, PHP, Rust. Your server renders them. A small hydration runtime (~14 kB min+gzip) makes them interactive. Node never ships.
"use client"
import { createSignal } from '@barefootjs/client'
export function Counter() {
const [count, setCount] = createSignal(0)
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count()}
</button>
)
}"use client"
import { createSignal } from '@barefootjs/client'
export function Toggle() {
const [on, setOn] = createSignal(false)
return (
<button aria-pressed={on()} onClick={() => setOn(v => !v)}>
{on() ? 'On' : 'Off'}
</button>
)
}export function Items({ items }: { items: string[] }) {
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}{{define "Counter"}}
{{if .Scripts}}{{.Scripts.Register "/static/client/barefoot.js"}}{{.Scripts.Register "/static/client/Counter.client.js"}}{{end}}
<button bf-s="{{bfScopeAttr .}}" {{bfHydrationAttrs .}} {{bfPropsAttr .}}{{if .BfDataKey}} data-key="{{.BfDataKey}}"{{end}} bf="s1"> Count: {{bfTextStart "s0"}}{{.Count}}{{bfTextEnd}}</button>
{{end}}<%- bf.register_script('/static/components/barefoot.js') -%>
<%- bf.register_script('/static/components/Counter.client.js') -%>
<button bf-s="<%= bf.scope_attr %>" <%= bf.hydration_attrs %> <%= bf.props_attr %> <%= bf.data_key_attr %> bf="s1"> Count: <%= bf.text_start("s0") %><%= bf.h(v[:count]) %><%= bf.text_end %></button>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Counter.client.js') %}
<button bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | safe }} {{ bf.props_attr() | safe }} {{ bf.data_key_attr() | safe }} bf="s1"> Count: {{ bf.text_start("s0") | safe }}{{ bf.string(count) }}{{ bf.text_end() | safe }}</button>% bf->register_script('/static/components/barefoot.js');
% bf->register_script('/static/components/Counter.client.js');
<button bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %> <%== bf->data_key_attr %> bf="s1"> Count: <%== bf->text_start("s0") %><%= $count %><%== bf->text_end %></button>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Counter.client.js') %}
<button bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | raw }} {{ bf.props_attr() | raw }} {{ bf.data_key_attr() | raw }} bf="s1"> Count: {{ bf.text_start("s0") | raw }}{{ bf.string(count) }}{{ bf.text_end() | raw }}</button>@php($bf->register_script('/static/components/barefoot.js'))
@php($bf->register_script('/static/components/Counter.client.js'))
<button bf-s="{!! e($bf->scope_attr()) !!}" {!! $bf->hydration_attrs() !!} {!! $bf->props_attr() !!} {!! $bf->data_key_attr() !!} bf="s1"> Count: {!! $bf->text_start("s0") !!}{!! e($bf->string($count)) !!}{!! $bf->text_end() !!}</button>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Counter.client.js') %}
<button bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | safe }} {{ bf.props_attr() | safe }} {{ bf.data_key_attr() | safe }} bf="s1"> Count: {{ bf.text_start("s0") | safe }}{{ bf.string(count) }}{{ bf.text_end() | safe }}</button>: my $_bf_reg0 = $bf.register_script('/static/components/barefoot.js');
: my $_bf_reg1 = $bf.register_script('/static/components/Counter.client.js');
<button bf-s="<: $bf.scope_attr() :>" <: $bf.hydration_attrs() | mark_raw :> <: $bf.props_attr() | mark_raw :> <: $bf.data_key_attr() | mark_raw :> bf="s1"> Count: <: $bf.text_start("s0") | mark_raw :><: $count :><: $bf.text_end() | mark_raw :></button>import { bfText, bfTextEnd } from '@barefootjs/hono/utils'
import { createSignal } from '@barefootjs/hono/client-shim'
export function Counter({ __instanceId, __bfScope: _bfScope, __bfChild, __bfParentProps: _bfParentProps, __bfParent, __bfMount, "data-key": __dataKey }: { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfParentProps?: string; __bfParent?: string; __bfMount?: string; "data-key"?: string | number } = {} as { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfParentProps?: string; __bfParent?: string; __bfMount?: string; "data-key"?: string | number }) {
const __scopeId = __instanceId || `Counter_${Math.random().toString(36).slice(2, 8)}`
const count = () => 0
return (
<button onClick={() => {}} bf-s={__scopeId} {...(__bfParent ? { "bf-h": __bfParent } : {})} {...(__bfMount ? { "bf-m": __bfMount } : {})} {...(!__bfChild ? { "bf-r": "" } : {})} {...(__dataKey !== undefined ? { "data-key": __dataKey } : {})} bf="s1"> Count: {bfText("s0")}{count()}{bfTextEnd()}</button>
)
}{{define "Toggle"}}
{{if .Scripts}}{{.Scripts.Register "/static/client/barefoot.js"}}{{.Scripts.Register "/static/client/Toggle.client.js"}}{{end}}
<button aria-pressed="{{.On}}" bf-s="{{bfScopeAttr .}}" {{bfHydrationAttrs .}} {{bfPropsAttr .}}{{if .BfDataKey}} data-key="{{.BfDataKey}}"{{end}} bf="s1">{{if .On}}{{bfComment "cond-start:s0"}}{{"On"}}{{bfComment "cond-end:s0"}}{{else}}{{bfComment "cond-start:s0"}}{{"Off"}}{{bfComment "cond-end:s0"}}{{end}}</button>
{{end}}<%- bf.register_script('/static/components/barefoot.js') -%>
<%- bf.register_script('/static/components/Toggle.client.js') -%>
<button aria-pressed="<%= bf.h(bf.bool_str(v[:on])) %>" bf-s="<%= bf.scope_attr %>" <%= bf.hydration_attrs %> <%= bf.props_attr %> <%= bf.data_key_attr %> bf="s1">
<%- if bf.truthy?(v[:on]) -%>
<%= bf.comment("cond-start:s0") %><%= bf.h('On') %><%= bf.comment("cond-end:s0") %>
<%- else -%>
<%= bf.comment("cond-start:s0") %><%= bf.h('Off') %><%= bf.comment("cond-end:s0") %>
<%- end -%>
</button>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Toggle.client.js') %}
<button aria-pressed="{{ bf.bool_str(on) }}" bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | safe }} {{ bf.props_attr() | safe }} {{ bf.data_key_attr() | safe }} bf="s1">
{% if bf.truthy(on) %}
{{ bf.comment("cond-start:s0") | safe }}{{ bf.string('On') }}{{ bf.comment("cond-end:s0") | safe }}
{% else %}
{{ bf.comment("cond-start:s0") | safe }}{{ bf.string('Off') }}{{ bf.comment("cond-end:s0") | safe }}
{% endif %}
</button>% bf->register_script('/static/components/barefoot.js');
% bf->register_script('/static/components/Toggle.client.js');
<button aria-pressed="<%= bf->bool_str($on) %>" bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %> <%== bf->data_key_attr %> bf="s1">
% if ($on) {
<%== bf->comment("cond-start:s0") %><%= 'On' %><%== bf->comment("cond-end:s0") %>
% } else {
<%== bf->comment("cond-start:s0") %><%= 'Off' %><%== bf->comment("cond-end:s0") %>
% }
</button>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Toggle.client.js') %}
<button aria-pressed="{{ bf.bool_str(on) }}" bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | raw }} {{ bf.props_attr() | raw }} {{ bf.data_key_attr() | raw }} bf="s1">
{% if bf.truthy(on) %}
{{ bf.comment("cond-start:s0") | raw }}{{ bf.string('On') }}{{ bf.comment("cond-end:s0") | raw }}
{% else %}
{{ bf.comment("cond-start:s0") | raw }}{{ bf.string('Off') }}{{ bf.comment("cond-end:s0") | raw }}
{% endif %}
</button>@php($bf->register_script('/static/components/barefoot.js'))
@php($bf->register_script('/static/components/Toggle.client.js'))
<button aria-pressed="{!! e($bf->bool_str($on)) !!}" bf-s="{!! e($bf->scope_attr()) !!}" {!! $bf->hydration_attrs() !!} {!! $bf->props_attr() !!} {!! $bf->data_key_attr() !!} bf="s1">
@if($bf->truthy($on))
{!! $bf->comment("cond-start:s0") !!}{!! e($bf->string('On')) !!}{!! $bf->comment("cond-end:s0") !!}
@else
{!! $bf->comment("cond-start:s0") !!}{!! e($bf->string('Off')) !!}{!! $bf->comment("cond-end:s0") !!}
@endif
</button>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Toggle.client.js') %}
<button aria-pressed="{{ bf.bool_str(on) }}" bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | safe }} {{ bf.props_attr() | safe }} {{ bf.data_key_attr() | safe }} bf="s1">
{% if bf.truthy(on) %}
{{ bf.comment("cond-start:s0") | safe }}{{ bf.string('On') }}{{ bf.comment("cond-end:s0") | safe }}
{% else %}
{{ bf.comment("cond-start:s0") | safe }}{{ bf.string('Off') }}{{ bf.comment("cond-end:s0") | safe }}
{% endif %}
</button>: my $_bf_reg0 = $bf.register_script('/static/components/barefoot.js');
: my $_bf_reg1 = $bf.register_script('/static/components/Toggle.client.js');
<button aria-pressed="<: $bf.bool_str($on) :>" bf-s="<: $bf.scope_attr() :>" <: $bf.hydration_attrs() | mark_raw :> <: $bf.props_attr() | mark_raw :> <: $bf.data_key_attr() | mark_raw :> bf="s1">
: if ($on) {
<: $bf.comment("cond-start:s0") | mark_raw :><: 'On' :><: $bf.comment("cond-end:s0") | mark_raw :>
: } else {
<: $bf.comment("cond-start:s0") | mark_raw :><: 'Off' :><: $bf.comment("cond-end:s0") | mark_raw :>
: }
</button>import { bfComment } from '@barefootjs/hono/utils'
import { createSignal } from '@barefootjs/hono/client-shim'
export function Toggle({ __instanceId, __bfScope: _bfScope, __bfChild, __bfParentProps: _bfParentProps, __bfParent, __bfMount, "data-key": __dataKey }: { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfParentProps?: string; __bfParent?: string; __bfMount?: string; "data-key"?: string | number } = {} as { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfParentProps?: string; __bfParent?: string; __bfMount?: string; "data-key"?: string | number }) {
const __scopeId = __instanceId || `Toggle_${Math.random().toString(36).slice(2, 8)}`
const on = () => false
return (
<button aria-pressed={on()} onClick={() => {}} bf-s={__scopeId} {...(__bfParent ? { "bf-h": __bfParent } : {})} {...(__bfMount ? { "bf-m": __bfMount } : {})} {...(!__bfChild ? { "bf-r": "" } : {})} {...(__dataKey !== undefined ? { "data-key": __dataKey } : {})} bf="s1">{on() ? <>{bfComment("cond-start:s0")}{'On'}{bfComment("cond-end:s0")}</> : <>{bfComment("cond-start:s0")}{'Off'}{bfComment("cond-end:s0")}</>}</button>
)
}{{define "Items"}}
<ul bf-s="{{bfScopeAttr .}}" {{bfHydrationAttrs .}} {{bfPropsAttr .}}{{if .BfDataKey}} data-key="{{.BfDataKey}}"{{end}} bf="s1">{{bfComment "loop:l0"}}{{range $_, $item := .Items}}<li data-key="{{.}}">{{bfTextStart "s0"}}{{.}}{{bfTextEnd}}</li>{{end}}{{bfComment "/loop:l0"}}</ul>
{{end}}<%- bf.register_script('/static/components/barefoot.js') -%>
<%- bf.register_script('/static/components/Items.client.js') -%>
<ul bf-s="<%= bf.scope_attr %>" <%= bf.hydration_attrs %> <%= bf.props_attr %> <%= bf.data_key_attr %> bf="s1"><%= bf.comment("loop:l0") %>
<%- (0...v[:items].length).each do |_i| -%>
<%- item = v[:items][_i] -%>
<li data-key="<%= bf.h(item) %>"><%= bf.text_start("s0") %><%= bf.h(item) %><%= bf.text_end %></li>
<%- end -%>
<%= bf.comment("/loop:l0") %></ul>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Items.client.js') %}
<ul bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | safe }} {{ bf.props_attr() | safe }} {{ bf.data_key_attr() | safe }} bf="s1">{{ bf.comment("loop:l0") | safe }}
{% for item in items %}
<li data-key="{{ bf.string(item) }}">{{ bf.text_start("s0") | safe }}{{ bf.string(item) }}{{ bf.text_end() | safe }}</li>
{% endfor %}
{{ bf.comment("/loop:l0") | safe }}</ul>% bf->register_script('/static/components/barefoot.js');
% bf->register_script('/static/components/Items.client.js');
<ul bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %> <%== bf->data_key_attr %> bf="s1"><%== bf->comment("loop:l0") %>
% for my $_i (0..$#{$items}) {
% my $item = $items->[$_i];
<li data-key="<%= $item %>"><%== bf->text_start("s0") %><%= $item %><%== bf->text_end %></li>
% }
<%== bf->comment("/loop:l0") %></ul>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Items.client.js') %}
<ul bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | raw }} {{ bf.props_attr() | raw }} {{ bf.data_key_attr() | raw }} bf="s1">{{ bf.comment("loop:l0") | raw }}
{% for item in items %}
<li data-key="{{ bf.string(item) }}">{{ bf.text_start("s0") | raw }}{{ bf.string(item) }}{{ bf.text_end() | raw }}</li>
{% endfor %}
{{ bf.comment("/loop:l0") | raw }}</ul>@php($bf->register_script('/static/components/barefoot.js'))
@php($bf->register_script('/static/components/Items.client.js'))
<ul bf-s="{!! e($bf->scope_attr()) !!}" {!! $bf->hydration_attrs() !!} {!! $bf->props_attr() !!} {!! $bf->data_key_attr() !!} bf="s1">{!! $bf->comment("loop:l0") !!}
@foreach(($items ?? []) as $item)
<li data-key="{!! e($bf->string($item)) !!}">{!! $bf->text_start("s0") !!}{!! e($bf->string($item)) !!}{!! $bf->text_end() !!}</li>
@endforeach
{!! $bf->comment("/loop:l0") !!}</ul>{% set _bf_reg0 = bf.register_script('/static/components/barefoot.js') %}
{% set _bf_reg1 = bf.register_script('/static/components/Items.client.js') %}
<ul bf-s="{{ bf.scope_attr() }}" {{ bf.hydration_attrs() | safe }} {{ bf.props_attr() | safe }} {{ bf.data_key_attr() | safe }} bf="s1">{{ bf.comment("loop:l0") | safe }}
{% for item in items %}
<li data-key="{{ bf.string(item) }}">{{ bf.text_start("s0") | safe }}{{ bf.string(item) }}{{ bf.text_end() | safe }}</li>
{% endfor %}
{{ bf.comment("/loop:l0") | safe }}</ul>: my $_bf_reg0 = $bf.register_script('/static/components/barefoot.js');
: my $_bf_reg1 = $bf.register_script('/static/components/Items.client.js');
<ul bf-s="<: $bf.scope_attr() :>" <: $bf.hydration_attrs() | mark_raw :> <: $bf.props_attr() | mark_raw :> <: $bf.data_key_attr() | mark_raw :> bf="s1"><: $bf.comment("loop:l0") | mark_raw :>
: for $items -> $item {
<li data-key="<: $item :>"><: $bf.text_start("s0") | mark_raw :><: $item :><: $bf.text_end() | mark_raw :></li>
: }
<: $bf.comment("/loop:l0") | mark_raw :></ul>import { bfComment, bfText, bfTextEnd } from '@barefootjs/hono/utils'
type ItemsPropsWithHydration = { items: string[] } & {
__instanceId?: string
__bfScope?: string
__bfChild?: boolean
__bfParentProps?: string
__bfParent?: string
__bfMount?: string
"data-key"?: string | number
}
export function Items({ items, __instanceId, __bfScope: _bfScope, __bfChild, __bfParentProps, __bfParent, __bfMount, "data-key": __dataKey }: ItemsPropsWithHydration) {
const __scopeId = __instanceId || `Items_${Math.random().toString(36).slice(2, 8)}`
// Serialize props for client hydration
const __hydrateProps: Record<string, unknown> = {}
if (typeof items !== 'function' && !(typeof items === 'object' && items !== null && 'isEscaped' in items)) __hydrateProps['items'] = items
const __bfPropsJson = __bfParentProps || (Object.keys(__hydrateProps).length > 0 ? JSON.stringify(__hydrateProps) : undefined)
return (
<ul bf-s={__scopeId} {...(__bfParent ? { "bf-h": __bfParent } : {})} {...(__bfMount ? { "bf-m": __bfMount } : {})} {...(!__bfChild ? { "bf-r": "" } : {})} {...(!__bfChild && __bfPropsJson ? { "bf-p": __bfPropsJson } : {})} {...(__dataKey !== undefined ? { "data-key": __dataKey } : {})} bf="s1">{bfComment('loop:l0')}{items.map((item) => <li key={item} data-key={String(item)}>{bfText("s0")}{item}{bfTextEnd()}</li>)}{bfComment('/loop:l0')}</ul>
)
}It's a compiler, not a framework. TSX and type-checking exist at build time, like a Sass compiler. At runtime there is only your template engine and one small ~14 kB min+gzip hydration script.
Two good answers. Now a third.
For interactive UI on a non-Node backend, two approaches serve teams well today. BarefootJS adds a third — for one specific situation: you want a component model, and you want to keep the backend you have.
Server templates + Alpine or Stimulus
A great fit for light interactivity — simple, close to HTML, easy to adopt. As client state grows and needs to be shared, typed, and tested, keeping it organized takes more and more care.
Adopt a JS meta-framework
Next.js and Remix offer an excellent component model, and they shine when a team runs JavaScript end to end. If your current backend is serving you well, though, getting there means a migration and a new runtime to operate.
Compile components into your templates
Typed props, signals, and composition when you write. Your own template engine when you serve. Build-time type errors instead of runtime bug reports — and nothing new to operate.
62 components × 9 adapters, verified in CI
Every cell below is a real check that runs on every commit — not an illustration.
Is this for you?
Probably yes, if
- Your backend is Go, Rails, Django, Perl, PHP, or Rust — and it's serving you well
- Your UI has outgrown sprinkles, but a Node migration isn't worth it
- You want UI that agents and CI can verify — structural tests in milliseconds, every CLI command speaks
--json
Probably no, if
- Next.js or Remix is working well for you — you already have a great component model
- You're building an app-like SPA: client routing, offline state, canvas editors
- Your pages have no interactivity — plain templates are already right
Try it in one command
Scaffolds a working project — pick your language and framework in the prompt.