Pages
Home
Playground
Integrations
Docs
Introduction
Quick Start
Core Concepts
Backend Freedom
MPA-style Development
Fine-grained Reactivity
AI-native Development
How It Works
Reactivity
createSignal
createEffect
createMemo
onMount
onCleanup
untrack
batch
Props Reactivity
Templates & Rendering
JSX Compatibility
Fragment
Client Directive
Components
Component Authoring
Props & Type Safety
Children & Slots
Context API
Portals
Style Overrides
Adapters
Adapter Architecture
Hono Adapter
Go Template Adapter
Perl Adapter
Ruby Adapter
Python Adapter
PHP Adapter
Rust Adapter
Custom Adapter
Advanced
Compiler Internals
IR Schema
Error Codes
Performance
Compatibility Matrix

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.

what you write
"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>
  )
}
what your server renders
counter.tmpl
{{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}}
counter.erb
<%- 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>
counter.jinja
{% 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>
counter.html.ep
% 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>
counter.twig
{% 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>
counter.blade.php
@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>
counter.j2
{% 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>
counter.tx
: 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>
Counter.tsx
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>
  )
}
toggle.tmpl
{{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}}
toggle.erb
<%- 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>
toggle.jinja
{% 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>
toggle.html.ep
% 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>
toggle.twig
{% 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>
toggle.blade.php
@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>
toggle.j2
{% 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>
toggle.tx
: 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>
Toggle.tsx
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>
  )
}
items.tmpl
{{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}}
items.erb
<%- 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>
items.jinja
{% 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>
items.html.ep
% 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>
items.twig
{% 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>
items.blade.php
@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>
items.j2
{% 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>
items.tx
: 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>
Items.tsx
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.

Option 1

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.

Option 2

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.

With BarefootJS

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.

hono
blade
erb
go-template
jinja
minijinja
mojolicious
twig
xslate

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.

$ npm create barefootjs@latest