Embedding Stoplight Elements in a React App
This guide is part of Stoplight Elements within Developer Portal Frameworks & UI Setup. It walks through embedding the @stoplight/elements <API> component inside a React + Vite application so you can ship an interactive API reference as a route in your own app rather than a standalone page. This scenario arises when you already own a React portal shell — navigation, auth, theming — and want the reference docs to live inside it instead of an iframe or a separate Redoc/Scalar deployment.
Problem & Context
A common failure mode is dropping the <API> component into a React tree and getting one of two broken states. First, an unstyled wall of text: the component renders the operation list and schemas but with no layout, because the stylesheet was never imported. Elements ships its CSS separately from the JS bundle, so importing the React component alone gets you markup with no design tokens.
Second, blank screens or 404s on refresh. Elements is internally routed — clicking an operation changes the URL. With the default history router on a static host, refreshing /docs/operations/get-users asks the server for a file that does not exist, returning a 404. The fix is choosing the right router prop for your hosting and embedding model, which this guide makes explicit.
Step-by-Step Solution
1. Scaffold a Vite React app
npm create vite@latest api-portal -- --template react
cd api-portal
npm install
Expected output ends with:
Scaffolding project in ./api-portal...
Done. Now run:
npm run dev
2. Install Stoplight Elements
Pin the major version — Elements 7.x and 8.x differ in CSS token names, so unpinned installs cause silent theme drift:
npm install @stoplight/[email protected]
Verify the resolved version:
npm ls @stoplight/elements
# [email protected]
# └── @stoplight/[email protected]
3. Import the stylesheet at the app entry point
Import the CSS exactly once, at the top of src/main.jsx, so it loads before any <API> mounts:
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import '@stoplight/elements/styles.min.css' // required — ships separately from the JS
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
If you forget this line, the component renders but appears as raw, unstyled HTML.
4. Render the <API> component
Use apiDescriptionUrl to point at a spec, and set router="hash" so deep links survive a page refresh on a static host:
// src/App.jsx
import { API } from '@stoplight/elements'
export default function App() {
return (
<div style={{ height: '100vh' }}>
<API
apiDescriptionUrl="/openapi.yaml" // fetched at runtime from public/
router="hash" // URLs become /#/operations/... — refresh-safe
layout="sidebar" // sidebar | stacked
/>
</div>
)
}
Give the wrapper an explicit height. Elements fills its container, and a zero-height parent collapses the whole reference to nothing.
5. Serve the spec and run the dev server
Place your OpenAPI document in public/ so Vite serves it at the site root:
cp ../specs/openapi.yaml public/openapi.yaml
npm run dev
Expected output:
VITE v5.4.0 ready in 412 ms
➜ Local: http://localhost:5173/
Open http://localhost:5173/ and you should see the sidebar reference. Clicking an operation changes the URL to http://localhost:5173/#/operations/getUsers, and refreshing keeps you on that operation.
Complete Working Example
A single self-contained App.jsx that handles the load state, lets you switch routers via a prop, and embeds Elements as a route-friendly component:
// src/App.jsx — complete, copy-paste ready
import { useState } from 'react'
import { API } from '@stoplight/elements'
// router: "hash" for a standalone docs page on a static host (refresh-safe),
// "memory" when this component lives inside another routed view and
// must NOT touch the browser URL.
const ROUTER_MODE = 'hash'
export default function App() {
const [specUrl] = useState('/openapi.yaml')
return (
<div className="app-shell" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<header style={{ padding: '0 16px', borderBottom: '1px solid #d8dff2' }}>
<h1 style={{ fontSize: 18, color: '#16295d' }}>Acme API Reference</h1>
</header>
<main style={{ flex: 1, minHeight: 0 /* lets Elements scroll inside flex */ }}>
<API
apiDescriptionUrl={specUrl}
router={ROUTER_MODE}
layout="sidebar"
hideExport={false} // show the "Export" / download-spec control
tryItCredentialsPolicy="same-origin" // omit cookies on cross-origin Try It calls
/>
</main>
</div>
)
}
To embed inside an existing React Router app instead, mount this on a wildcard route and switch ROUTER_MODE to 'memory' so Elements manages its own internal navigation without fighting your top-level router:
// In your route config (react-router-dom v6)
<Route path="/docs/*" element={<App />} />
Gotchas & Edge Cases
Collapsed-to-nothing layout. Elements has no intrinsic height. If the reference is invisible but present in the DOM, the parent container has zero height. Set height: 100vh (or flex: 1 with minHeight: 0) on the wrapper.
Nested router URL collisions. Using router="history" inside an app that already runs React Router means two routers compete for the same URL, producing flicker and broken back-button behaviour. Inside a routed app, use router="memory"; for a dedicated docs subdomain or static host, use router="hash".
Try-It CORS failures. The interactive console fires real requests from the browser. If the API server lacks Access-Control-Allow-Origin for your docs origin, calls fail with a CORS error in the console even though the spec renders fine. Add the docs origin to the API’s allowed origins, or set a corsProxy on the <API> component for non-production environments.
FAQ
Why is my Stoplight Elements component unstyled?
You imported the component but not its stylesheet. Add import '@stoplight/elements/styles.min.css' once at your app entry point so the bundled tokens and layout load.
Should I use hash or memory routing for Stoplight Elements?
Use router="hash" when Elements owns its own URL space and you want deep links to operations. Use router="memory" when you embed Elements inside another routed view and do not want it to touch the browser URL.
Can I pass a raw OpenAPI object instead of a URL?
Yes. Use the apiDescriptionDocument prop with a parsed object or JSON string instead of apiDescriptionUrl. This avoids a network round trip when the spec is already bundled.
Related
- Stoplight Elements — parent guide and configuration reference
- Scalar & Modern Docs Integration — an alternative embeddable reference renderer
- Swagger UI Customization — for teams comparing embed options
- Developer Portal Frameworks & UI Setup — the parent overview