Build a custom skin
Build a Hyperflow UI with your own design system and the headless React layer.
Use a custom skin when you want to replace the Chakra UI presentation but keep Hyperflow's client, routing, session state, and real-time behavior. You provide the components and styles; @hyperflow/react provides the application contracts.
Before you start, obtain apiUrl and wsUrl for your Hyperflow runtime. Runtime setup and authentication infrastructure are outside the scope of this guide.
1. Install the headless packages
Install the React layer, its router peer dependency, and the Vite plugin.
pnpm add @hyperflow/react react-router@^7
pnpm add -D @hyperflow/viteRegister the plugin before the React plugin:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { hyperflowVitePlugin } from '@hyperflow/vite'
export default defineConfig({
plugins: [hyperflowVitePlugin(), react()],
})2. Create the application shell
AppCore initializes Hyperflow, installs the router and keeps sessions synchronized. Give it your layout, login component, and routes.
import type { ReactNode } from 'react'
import { AppCore, type HyperflowRoute } from '@hyperflow/react'
import { NewSessionPage, SessionPage } from './pages/sessions'
function AppLayout({ children }: { children: ReactNode }) {
return <main>{children}</main>
}
function LoginPage() {
return <p>Render your sign-in experience here.</p>
}
const routes: HyperflowRoute[] = [
{ path: 'sessions/new', component: NewSessionPage },
{ path: 'sessions/:sessionId', component: SessionPage },
]
export function App() {
return (
<AppCore
config={{
apiUrl: 'https://runtime.example.com/api/v1',
wsUrl: 'wss://runtime.example.com/api/v1/ws',
}}
layoutComponent={AppLayout}
loginComponent={LoginPage}
routes={routes}
/>
)
}Replace the example URLs with the values provided for your environment. For a same-origin setup, use { apiUrl: '/api/v1', wsUrl: '/api/v1/ws' }.
AppCore owns the HyperflowProvider, router, authentication guard, and session synchronization bridges. Do not add another HyperflowProvider or BrowserRouter around it.
3. Build pages with headless hooks
Use the hooks as the interface between Hyperflow behavior and your visual components. For example, a composer page can use the route-aware useComposer contract directly.
import { useParams } from 'react-router'
import { useComposer, useTimeline } from '@hyperflow/react'
export function NewSessionPage() {
return <Conversation />
}
export function SessionPage() {
return <Conversation />
}
function Conversation() {
const { sessionId } = useParams()
const composer = useComposer()
const timeline = useTimeline(sessionId)
return (
<section>
{timeline.status.isLoading && <p>Loading messages…</p>}
<ol>
{timeline.entries.map((entry) => (
<li key={entry.id}>{entry.kind}</li>
))}
</ol>
<form onSubmit={composer.send.onSubmit}>
<textarea {...composer.input.props} />
<button disabled={!composer.send.canSubmit} type="submit">
{composer.send.isBusy ? 'Sending…' : 'Send'}
</button>
</form>
{composer.error && <p role="alert">{composer.error.message}</p>}
</section>
)
}The example deliberately renders only each timeline entry's kind. Replace that list with components for the entry types your product supports, such as user and assistant messages, tool groups, thinking, approvals, and errors.
Choose the right hook
| UI area | Use |
|---|---|
| Message input, attachments, send, stop, agent/model controls | useComposer |
| Historical messages, streamed output, loading and pagination | useTimeline |
| Tool approval, tool input and agent-auth requests | useHitl |
| Session list, groups, create, rename and delete | useSidebar |
| Current session data | useSession |
Compose these hooks into semantic components. Do not call the client from presentation components or import internal Zustand stores: the hooks keep a skin independent from Hyperflow's internal state implementation.
Next steps
- Handle interactive runtime requests with Human-in-the-loop.
- See the React reference for the public headless contracts.