Docs

Frameworks

Nifra renders five UI frameworks on one agnostic core. The render seam, file-based routes, typed loaders/actions, streaming, islands, prefetch, and the client router are shared across adapters, while each page still uses its framework's normal component style.

Scaffold any of them

create-nifra's --framework flag scaffolds the multi-target SSR site with the adapter, routes, build wiring, and deps for your pick. It composes with --deploy, so one command gives you a framework + a default deploy target.

TS
# Scaffold a multi-target SSR site in any of the five (react is the default):
bun create nifra my-app --framework solid      # or react · preact · vue · svelte

# Composes with the deploy preset — pick a framework AND a default deploy target:
bun create nifra my-app --framework svelte --deploy vercel

The adapters

FrameworkPackageUI idiomBuild pluginHydration bundle
Solid@nifrajs/web-solidprimitivesBabel~15 KB
Preact@nifrajs/web-preacthooks (React-compat)none~18 KB
Svelte 5@nifrajs/web-sveltestores + runes.svelte compiler~49 KB
Vue 3@nifrajs/web-vuecomposables.vue SFC (or render fns)~66 KB
React 19@nifrajs/web-reacthooksnone (Bun JSX)~182 KB
Bundle = the same minimal counter app, minified (not gzipped), for each framework — see examples/web-*. Indicative payload, not a benchmark.

Authoring routes

A route's default export is the component; its loader/action/meta are named exports. Most adapters write .tsx, but the compiled frameworks use their native single-file format — Svelte .svelte (loader/meta in <script module>) and Vue .vue SFCs (loader/meta in the plain <script>; the component in <script setup> + <template>) — each compiled by its package's Bun plugin (@nifrajs/web-vue/plugin, @nifrajs/web-svelte/plugin).

TS
<!-- routes/index.vue — a nifra route authored as a Vue Single-File Component -->
<script lang="ts">
// The plain <script> carries nifra's route convention (server-only named exports):
export const meta = { title: "Home" }
export async function loader({ api }) {
  const res = await api.count.get()
  return { count: res.data?.count ?? 0 }
}
</script>

<script setup lang="ts">
defineProps(["data"])          // nifra passes the loader data in as `data`
</script>

<template>
  <h1>Count: {{ data.count }}</h1>
</template>
Component <style scoped> (Vue) and <style> (Svelte — scoped by default) are compiled and scoped into the app stylesheet — no runtime, no FOUC — plus CSS Modules and global imports. See Dev & HMR → Styling. examples/routing-vue-sfc is a full SSR + hydration + client-nav demo.

Same features, every framework

Every adapter gets Nifra's full feature set — not a subset:

  • Streaming SSR + hydration (islands), with component-level Suspense.
  • File-based routing, nested layouts, typed loader/action, and progressive-enhancement forms (work with JS off).
  • <Await> for deferred data (defer()), a keyed query cache (useQuery), and concurrent useFetcher mutations.
  • Optimistic UI, targeted revalidation, hover-prefetch, and scroll restoration.
  • True HMR in dev for all five (@nifrajs/web/vite) — see Dev & HMR.

One line changes

The adapter is the only framework-specific choice on the server. Swap it and the entire app — routes, data, streaming — runs on a different framework, unchanged.

TS
// Server — pick an adapter. Everything else is identical across all five frameworks:
// the same routes, loaders, actions, streaming, <Await>, fetchers, query cache.
import { createWebApp } from "@nifrajs/web"
import { reactAdapter } from "@nifrajs/web-react"   // ← or web-solid · web-vue · web-preact · web-svelte

const app = createWebApp({
  adapter: reactAdapter,   // the one line that changes per framework
  manifest,
  clientEntry,             // the built client bundle (from buildClient's manifest)
  api,
})

Idiomatic, not lowest-common-denominator

The bindings share names and behaviour across frameworks, but each is expressed the native way — React/Preact hooks, Vue composables, Solid signals, Svelte stores.

TS
// The data primitives — same names + behaviour everywhere, each in the framework's idiom.
// React / Preact — hooks:
const fetcher = useFetcher("save")          // fetcher.submit(...), fetcher.pending
const q = useQuery(["todos"], loadTodos)    // q.data, q.isFetching, q.refetch()

// Vue — composables (refs):           const f = useFetcher("save"); f.state.value.pending
// Solid — signals:                    const q = useQuery(...);       q().data
// Svelte — stores (read with $):      const f = useFetcher("save");  $f.pending

Deferred data behavior

React, Preact, and Solid stream a <Await> boundary — the fallback flushes in the SSR HTML, then the resolved content streams in (it works with JS off). Vue and Svelte resolve deferred data on the client, so JS-off users see the fallback for that deferred boundary. Put critical page data in the route loader; use defer() for slower, non-critical data.

Dev HMR support

Dev HMR works for all five adapters through createViteDevServer and the framework's Vite plugin. React, Preact, and Vue preserve edited component state. Solid and Svelte update live without a full page reload, and the edited component may remount. Full matrix in Dev & HMR.

Proudly built with Nifra — server-rendered on Cloudflare Pages.MIT