Pages
Home
Playground
Integrations
Docs
Introduction
Quick Start
How It Works
Components
Component Authoring
Children & Slots
Context API
Portals
Style Overrides
Reactivity
createSignal
createEffect
createMemo
onMount
onCleanup
untrack
batch
createQuery
createMutation
Props Reactivity
JSX & Templates
JSX Compatibility
Client Directive
Adapters
Hono
Go Template
Perl
Ruby
Python
PHP
Rust
Java
CSR
Writing a Custom Adapter
Tooling & Reference
Vite Plugin
Testing & CLI
API Reference
Error Codes
Compatibility Matrix
Coming from React or Solid

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 (~16 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, __bfNoSerialize: _bfNoSerialize, __bfParentProps: _bfParentProps, __bfParent, __bfMount, "data-key": __dataKey }: { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfNoSerialize?: boolean; __bfParentProps?: string; __bfParent?: string; __bfMount?: string; "data-key"?: string | number } = {} as { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfNoSerialize?: 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, __bfNoSerialize: _bfNoSerialize, __bfParentProps: _bfParentProps, __bfParent, __bfMount, "data-key": __dataKey }: { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfNoSerialize?: boolean; __bfParentProps?: string; __bfParent?: string; __bfMount?: string; "data-key"?: string | number } = {} as { __instanceId?: string; __bfScope?: string; __bfChild?: boolean; __bfNoSerialize?: 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") %>
<%- v[:items].each_with_index do |item, _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, serializeHydrationProps } from '@barefootjs/hono/utils'

type ItemsPropsWithHydration = { items: string[] } & {
  __instanceId?: string
  __bfScope?: string
  __bfChild?: boolean
  __bfNoSerialize?: boolean
  __bfParentProps?: string
  __bfParent?: string
  __bfMount?: string
  "data-key"?: string | number
}

export function Items({ items, __instanceId, __bfScope: _bfScope, __bfChild, __bfNoSerialize, __bfParentProps, __bfParent, __bfMount, "data-key": __dataKey }: ItemsPropsWithHydration) {
  const __scopeId = __instanceId || `Items_${Math.random().toString(36).slice(2, 8)}`

  // Serialize props for client hydration — root mounts only. A child's bf-p
  // is never read (it gets props live via initChild), and __bfNoSerialize
  // says the same for a component that is its parent's entire JSX body, so
  // neither one pays for — or can fail SSR on — a value nothing will read.
  let __bfPropsJson = __bfParentProps
  if (!__bfChild && !__bfNoSerialize) {
    const __hydrateProps: Record<string, unknown> = {}
    if (!(typeof items === 'object' && items !== null && 'isEscaped' in items)) __hydrateProps['items'] = items
    __bfPropsJson = __bfParentProps || serializeHydrationProps(__hydrateProps, 'Items', {})
  }

  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 ~16 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 × 10 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
pebble
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