Framework support
The SDK ships a <BdForm> binding for eight targets. Each is a thin view
over the same framework-agnostic core, so the behaviour you rely on — values,
validation, conditional visibility, multi-step navigation, uploads, submit
lifecycle — is identical everywhere and doesn't change when your framework
does.
Forms are where the bindings are equal, and where the equality stops. The rest of the SDK — chat, customer auth, storefront, cart, checkout, blog, the customer portal, marketing pages, followers, social links and the verification flows — currently exists only for React. The table below is the honest version; read it before you pick a stack.
What each adapter can do
A capability is not a yes/no, and reporting it that way would flatter us. Two things are being distinguished:
- native — real components and hooks rendering into your DOM, styled by your own CSS.
- iframe — a BusinessDash-hosted page in a sandboxed iframe. It works, and it is often the fastest way to ship, but it renders our markup inside your app and your design system does not reach it.
- both — the surface exists in both forms. React's chat is the clearest example: an iframe widget and a headless hook for building your own UI.
| Capability | react | solid | vue | svelte | qwik | angular | vanilla | element |
|---|---|---|---|---|---|---|---|---|
| Forms | native | native | native | native | native | native | native | native |
| Shared client | native | native | native | native | — | native | — | native |
| AI chat | both | both | native | native | native | native | native | native |
| Storefront | both | both | native | native | native | native | native | native |
| Cart | both | both | native | native | native | native | native | native |
| Blog | both | both | native | native | native | native | native | native |
| Checkout | both | both | native | native | native | native | native | native |
| Customer portal | both | both | native | native | native | native | native | native |
| Customer auth | native | native | native | native | native | native | native | native |
| Followers | native | native | native | native | native | native | native | native |
| Marketing pages | native | native | native | native | native | native | native | native |
| Social links | native | native | native | native | native | native | native | native |
| Email verification | native | native | native | native | native | native | native | native |
| Phone verification | native | native | native | native | native | native | native | native |
| Surfaces | 14 | 14 | 14 | 14 | 13 | 14 | 13 | 14 |
Two things in that table are worth reading carefully, because they cut against the obvious summary.
Every capability is available on every adapter. The only cells that read
— are the shared-client primitive on qwik and vanilla, and neither needs
one: both take the client per form instead, which is the right shape for a
resumability-first framework and for an imperative mount.
Ten shared cores carry the behaviour — forms, store, chat, blog, checkout,
account, portal, marketing, verify and social — and each binding is a thin view
over them. React and Solid additionally offer the hosted iframe widgets, which
is why several rows read both.
Nothing is iframe-only. Where an iframe still exists it is an alternative, never the only option.
The customer portal
The largest surface here: 34 methods covering work, quotes, contracts, invoices, orders, downloads, shipping, reviews, referrals, profile and notification preferences. Three things about it are worth knowing before you build against it, and the SDK does not paper over any of them.
sessionToken is required. Every portal route is scoped to the signed-in
customer, and a portal built without one renders empty rather than signed-out —
which looks like a bug in your code, not a missing token. The controller takes
it up front and binds it for you.
There are no list endpoints for invoices, quotes, contracts or shipments.
work carries the counts and you open each by id. The SDK exposes exactly what
exists rather than faking a list from repeated detail fetches.
A submitted review is queued for staff approval, so it will not appear in
reviews. Watch reviewAwaitingModeration so you can tell the customer it is
pending rather than implying it is live.
React remains ahead on <ChatbotInline> — a rendered chat UI, roughly 1,200
lines, on top of the same headless controller everyone now shares. The
behaviour is common; only that component is React-only.
qwik and vanilla have no shared-client entry because neither needs one:
both take the client per form — a client prop and a mountBdForm option
respectively — which is the right shape for a resumability-first framework and
for an imperative mount.
If you need more than forms on a non-React framework, you have two practical routes today:
- Talk to the API directly. Every capability above is a thin view over the
package API, and
BdClient(from the root export) is framework-agnostic —client.storefront(),client.cart(),client.customerPortal()and the rest all work anywhere JavaScript runs. You write the markup; the data layer is already there. - Embed the web component.
@businessdash/sdk/elementregisters<bd-form>, which renders inside any framework.
This table is generated from capabilities in
@businessdash/sdk/framework-support, and a release gate asserts the code
matches it — so it cannot drift into promising something an adapter doesn't
export.
React-compatible targets
Next, Astro, Remix, React Router, TanStack Start and Preact are not
separate adapters and don't need to be. They render React, or a drop-in
replacement for it, so they import @businessdash/sdk/react and get all
fourteen capabilities. Choosing Next, Astro or Preact is not choosing a
smaller SDK.
| Framework | Import | What it needs | Starter |
|---|---|---|---|
| React | @businessdash/sdk/react | Nothing — import and render. | React-Bun |
| Next.js (App Router) | @businessdash/sdk/react | Nothing, as of 0.9.61. | T3-App |
| Astro | @businessdash/sdk/react | @astrojs/react, plus a client directive on the island. | Astro |
| Remix / React Router 7 | @businessdash/sdk/react | Nothing. | Remix |
| TanStack Start | @businessdash/sdk/react | Nothing. | Tanstack-Start |
| Preact | @businessdash/sdk/react | The standard preact/compat alias. | — |
| React Native / Expo | @businessdash/sdk/native | The native entry — DOM components don't render there. | React-Native |
Preact
Preact gets the full surface for the cost of a bundler alias. The React binding
imports nine runtime symbols — createContext, Fragment, useCallback,
useContext, useEffect, useMemo, useRef, useState,
useSyncExternalStore — all of which preact/compat provides, and it never
imports react-dom, so nothing drags in React's reconciler.
// vite.config.js
import preact from '@preact/preset-vite'
export default {
plugins: [preact()], // aliases react -> preact/compat for you
}
If you're wiring the alias by hand:
resolve: {
alias: { react: 'preact/compat', 'react-dom': 'preact/compat' },
}
This isn't a claim we make by inspection. A release gate bundles the real SDK
source twice — once against React, once with react aliased to preact/compat
— renders the same component trees on both, and asserts the markup is
identical. The only permitted difference is inline-style serialisation, where
React writes margin:0 and Preact writes margin:0px for the same computed
value.
Next.js and "use client"
The React adapter carries its own "use client" boundary, so you can import it
straight into a server-rendered page:
// app/contact/page.tsx — a Server Component
import { BdForm } from '@businessdash/sdk/react'
export default function ContactPage() {
return <BdForm slug="contact" />
}
Before 0.9.61 this threw "you're importing a component that needs useState",
and the workaround was a hand-written "use client" wrapper for every
component you used. Those wrappers still work — you don't have to remove them —
but new code doesn't need them.
Astro and client directives
Astro renders islands to static HTML unless you ask for hydration, and a form with no JavaScript attached will look correct and do nothing:
---
import ContactForm from '../components/ContactForm.tsx'
---
<ContactForm client:load />
Use client:load for anything interactive above the fold, client:visible for
forms further down the page.
Supported versions
| Import | Framework | Versions | Ships as | Status |
|---|---|---|---|---|
@businessdash/sdk/react | react | >=18 <20 | compiled | Supported |
@businessdash/sdk/solid | solid-js | >=1.8 <2 | source | Supported (1.x) |
@businessdash/sdk/vue | vue | >=3.3 <4 | source | Supported |
@businessdash/sdk/svelte | svelte | >=5 <6 | source | Supported (runes) |
@businessdash/sdk/qwik | @builder.io/qwik | >=1.5 <2 | source | Untested |
@businessdash/sdk/angular | @angular/core | >=17 | source | Untested |
@businessdash/sdk/vanilla | — | any | compiled | Supported |
@businessdash/sdk/element | — | any | compiled | Supported |
These ranges are declared as optional peerDependencies, so your package
manager warns you on a mismatch instead of failing at build time with a
confusing error.
The same data is importable if you want to assert on it in your own tooling:
import { FRAMEWORK_SUPPORT, supportFor } from '@businessdash/sdk/framework-support'
supportFor('solid-js') // every adapter dialect for Solid
When your framework ships a breaking major
Frameworks rework their own APIs, and an adapter written against the old dialect won't compile against the new one. When that happens we add a second adapter for the new major rather than replacing the old one, and this page lists both. Code on either major keeps working, and you upgrade when you choose to.
Two things limit the damage:
The core is framework-agnostic. Only the binding layer — how external state becomes reactive, and how a list renders — is version-sensitive. Nothing about how forms actually behave lives there.
@businessdash/sdk/element is always available. It's a web component with
no framework dependency, it works inside any framework, and it's the fastest
escape hatch if you're on a brand-new major before its adapter lands.
Solid 2.0
Solid 2.0 shipped in August 2026 with a substantial API rework. The current
@businessdash/sdk/solid adapter is written against the 1.x dialect and is
not yet verified on 2.x.
If you're on Solid 2.0 today, use @businessdash/sdk/element or
@businessdash/sdk/vanilla — both are framework-independent and give you the
same form behaviour — and watch this page for the v2 adapter.
Non-JavaScript starters
Swift, Kotlin, Dart, PHP and Elixir consumers do not import
@businessdash/sdk — each starter carries a hand-written client library. As of
0.9.61 all six reach every product surface:
| Starter | Coverage |
|---|---|
| Swift (iOS/macOS) | 15/15 |
| Kotlin (Android) | 15/15 |
| Flutter / Dart | 15/15 |
| Laravel (PHP) | 15/15 |
| Phoenix (Elixir) | 15/15 |
| Vapor (Swift server) | 15/15 |
They started between 8 and 11. The shared gap was social links and the two
verification kinds — surfaces added to the JS side and never carried across,
which is why sdk/test/non-js-parity.test.ts now measures this on every
run. It reads each client's source and matches on request paths, the one
thing six languages have in common.
Full coverage means every surface is reachable from every language. It does not mean the ergonomics match, and the gate cannot tell you that. Read a green run as "nothing is missing", not "the starters are finished".
The social-platform table is generated
The 31 platforms — key, label, icon slug, URL prefix — come from
sdk/src/socials.ts and are emitted into all six languages by
pnpm gen:socials. Six hand-maintained copies would drift the first time a
platform was added, and the copy that drifted would be whichever language
nobody was using that week. pnpm gen:socials:check fails CI when they are
stale.
The resolver around that table is hand-written per language, because that is what a developer actually reads. Generate the data, hand-write the idiom.