Hyperflow UI
Guides

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/vite

Register the plugin before the React plugin:

vite.config.ts
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.

src/App.tsx
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.

src/pages/sessions.tsx
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 areaUse
Message input, attachments, send, stop, agent/model controlsuseComposer
Historical messages, streamed output, loading and paginationuseTimeline
Tool approval, tool input and agent-auth requestsuseHitl
Session list, groups, create, rename and deleteuseSidebar
Current session datauseSession

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

On this page