Stoplight Elements: Render OpenAPI as Interactive Docs
Stoplight Elements turns an OpenAPI document into a fully interactive, three-pane API reference with a built-in request console, served as a framework-agnostic web component or a React package. This guide is part of Developer Portal Frameworks & UI Setup and covers the <elements-api> element, the apiDescriptionUrl, router, and layout props, and how to host or embed the reference so docs rebuild automatically when the spec changes.
Key objectives:
- Mount
<elements-api>from a CDN or a bundled@stoplight/elementsinstall - Point it at a spec with
apiDescriptionUrl(or inlineapiDescriptionDocument) - Choose the right
routerandlayoutfor standalone pages versus embedded panes - Deploy a static reference from CI and verify it renders without console errors
For a deeper, framework-specific walkthrough, see Embedding Stoplight Elements in React.
Prerequisites & Environment Setup
Stoplight Elements is a client-side renderer. It needs a valid OpenAPI document reachable over HTTP and a place to serve static HTML and JavaScript. There is no server runtime: the component fetches the spec in the browser and renders it.
Requirements:
- Node.js 20 LTS if you bundle Elements with a build tool. The CDN approach needs no Node at build time, only a static host.
- A valid OpenAPI 2.0, 3.0, or 3.1 document. Validate it before shipping so render failures surface in CI, not in production.
- CORS access to the API if you want the Try It console to send live requests from the docs origin to the API origin.
Install the package when bundling into an app or static-site build:
npm install @stoplight/[email protected]
Pin the version. Elements ships frequent releases, and the bundled stylesheet and DOM structure can change between minor versions, which breaks custom CSS overrides if you float on latest.
Add a lint step so a malformed spec never reaches the renderer:
npx @redocly/cli@2 lint openapi.yaml
Elements renders a blank or partial page when the spec has unresolved $refs or missing required fields, with only a vague console warning. Linting first converts that silent failure into a hard CI error. The same discipline applies across renderers — see Redocly & OpenAPI UI Configuration for the equivalent gate in a Redoc pipeline.
Core Configuration
The simplest integration loads the web component and stylesheet from a CDN and mounts a single <elements-api> element. The component is a custom element registered by the Elements bundle; once the script loads, the browser upgrades the tag.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>API Reference</title>
<!-- Pin the version in the URL for reproducible rendering -->
<link
rel="stylesheet"
href="https://unpkg.com/@stoplight/[email protected]/styles.min.css"
/>
<script src="https://unpkg.com/@stoplight/[email protected]/web-components.min.js"></script>
</head>
<body style="height: 100vh; margin: 0;">
<elements-api
apiDescriptionUrl="/openapi.yaml"
router="hash"
layout="sidebar"
></elements-api>
</body>
</html>
The three attributes do the heavy lifting:
apiDescriptionUrlis the URL the component fetches and parses. It accepts a relative or absolute path to a JSON or YAML OpenAPI document. For specs assembled at build time, fetch the bundled output (a single dereferenced file) rather than a multi-file spec with external$refs, which the browser fetcher cannot always follow.routercontrols how navigation maps to the URL.hashstores the active operation after#and needs no server config — the safest default for static hosts.historyproduces clean paths but requires the host to rewrite unknown routes toindex.html.memorykeeps navigation entirely in component state and changes no URL, which is what you want for an embedded pane.layoutis eithersidebar(a left navigation tree with a content pane, the default for full-page references) orstacked(a single scrolling column, better for narrow embeds and mobile).
To inline the spec instead of fetching it — useful when the spec is generated server-side or you want to avoid a second network request — use apiDescriptionDocument with a JSON string, or set the property in JavaScript:
<elements-api id="docs" router="hash" layout="sidebar"></elements-api>
<script>
const el = document.getElementById('docs');
fetch('/openapi.json')
.then((r) => r.json())
.then((spec) => { el.apiDescriptionDocument = spec; });
</script>
Setting the property in JS (rather than the attribute) avoids escaping a large JSON blob into HTML and lets you transform the spec — for example, stripping internal endpoints — before handing it to the renderer.
Other commonly used attributes: hideTryIt removes the interactive request panel for read-only public docs; hideSchemas collapses model definitions; tryItCredentialsPolicy (omit, include, or same-origin) controls whether the console sends cookies; and logo sets a custom mark in the sidebar header.
Integration Pattern
For a production portal, build the static reference in CI so it redeploys whenever the spec merges. The pattern: lint the spec, bundle it into a single dereferenced file, copy the Elements assets and an HTML shell, then publish.
# .github/workflows/elements-docs.yml
name: Build Stoplight Elements Docs
on:
push:
branches: [main]
paths: ['openapi.yaml', 'docs/index.html']
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Lint OpenAPI spec
run: npx @redocly/cli@2 lint openapi.yaml
- name: Bundle to a single dereferenced file
run: npx @redocly/cli@2 bundle openapi.yaml --dereferenced --output dist/openapi.json
- name: Assemble static site
run: |
mkdir -p dist
cp docs/index.html dist/index.html
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
The index.html in docs/ is the shell from the Core Configuration section, with apiDescriptionUrl="/openapi.json" pointing at the bundled artifact the workflow writes to dist/. Bundling with --dereferenced flattens every $ref into one file so the browser never has to chase external references — the single most common cause of partial renders on static hosts.
If you deploy to GitHub Pages under a project subpath (/your-repo/), keep router="hash". The history router would generate paths the Pages host cannot resolve without a custom 404-to-index rewrite.
Advanced Options
Embedding inside React with @stoplight/elements. The package exports an <API> component. Import the component and its stylesheet, then pass props that mirror the web-component attributes (camelCased). Use router="memory" so Elements does not fight the host app’s router:
import { API } from '@stoplight/elements';
import '@stoplight/elements/styles.min.css';
export function ApiReference() {
return (
<API
apiDescriptionUrl="/openapi.json"
router="memory"
layout="sidebar"
/>
);
}
The full Next.js dynamic-import pattern, which avoids SSR errors from the browser-only bundle, is covered in Embedding Stoplight Elements in React.
Theming with CSS variables. Elements exposes design tokens as CSS custom properties scoped to the component. Override them in a stylesheet loaded after styles.min.css:
.sl-elements {
--color-primary: #16306d;
--color-primary-dark: #1f3473;
--font-prose: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
Target the documented --color-* and --font-* tokens rather than internal sl-* class names, which change between releases. For a portal that supports a theme toggle, drive these variables from the same token set as the rest of the shell — see Multi-Theme & Dark Mode Support.
Multiple versions on one host. Build a separate bundled spec and HTML shell per version into dist/v1/ and dist/v2/, then add a root landing page that links to each. Each shell’s apiDescriptionUrl points at the bundle in its own directory, so versions stay isolated and cacheable.
Verification & Testing
After every build, confirm the reference actually rendered rather than failing silently. Serve the output and check for the rendered DOM:
npx http-server dist/ -p 8080
# in another terminal:
curl -s http://localhost:8080/ | grep -q "elements-api" && echo "shell present"
A curl check only proves the shell shipped, not that the spec parsed. Elements renders client-side, so add a headless browser assertion in CI to catch parse failures:
npx [email protected] install --with-deps chromium
node -e "
const { chromium } = require('playwright');
(async () => {
const b = await chromium.launch();
const p = await b.newPage();
const errors = [];
p.on('pageerror', (e) => errors.push(e.message));
await p.goto('http://localhost:8080/');
await p.waitForSelector('.sl-elements', { timeout: 15000 });
await b.close();
if (errors.length) { console.error(errors); process.exit(1); }
console.log('Elements rendered with no page errors');
})();
"
The waitForSelector('.sl-elements') call fails the build if the component never mounted — the reliable signal that the spec was unparseable or the bundle failed to load. The pageerror listener catches runtime exceptions that would otherwise leave a blank page in production.
Troubleshooting
- Blank page, console shows “Failed to fetch” or a CORS error.
apiDescriptionUrlpoints at an origin that does not allow the docs origin. Either serve the spec from the same origin as the docs (bundle it intodist/as in the workflow above) or addAccess-Control-Allow-Originfor the docs domain on the spec host. Cannot resolve reference/ partial render with missing schemas. The spec has external$refs the browser fetcher cannot follow. Bundle withnpx @redocly/cli@2 bundle openapi.yaml --dereferenced --output openapi.jsonand pointapiDescriptionUrlat the single output file.window is not definedduring a Next.js or Astro build. The Elements bundle is browser-only and ran during SSR. Load it with a client-only dynamic import (next/dynamicwithssr: false, or anastro:clientdirective) so it executes only in the browser.- Try It console returns 401 even with credentials entered.
tryItCredentialsPolicydefaults toomit, so cookies are not sent. SettryItCredentialsPolicy="include"for cookie-based auth, and confirm the API returnsAccess-Control-Allow-Credentials: true.
FAQ
Should I use the web component or the React package?
Use the @stoplight/elements web component for plain HTML, server-rendered pages, or any non-React stack. Use the React package when you need to mount the reference inside an existing React or Next.js application and share routing or theming with the host app.
Does Stoplight Elements support OpenAPI 3.1?
Yes. Recent @stoplight/elements releases parse OpenAPI 2.0, 3.0, and 3.1 documents. Lint the spec with @redocly/cli or Spectral first so unsupported or malformed constructs fail in CI rather than rendering blank in the browser.
Why does the hash router break when I deploy to a subpath?
The hash router stores state after the # and works on any host without server config, but it conflicts with anchor-based deep links. Switch to router="memory" for embedded panes or router="history" with a configured basePath when serving from a subdirectory.
How do I hide the Try It console for a public, read-only reference?
Set the hideTryIt attribute on the elements-api element. This removes the interactive request panel while keeping schemas, examples, and descriptions, which is useful for unauthenticated public docs or when the API has no CORS-enabled sandbox.
Related
- Developer Portal Frameworks & UI Setup — the parent overview of portal renderers and hosting.
- Embedding Stoplight Elements in React — the framework-specific deep dive.
- Redocly & OpenAPI UI Configuration — a static-HTML alternative for enterprise references.
- Scalar & Modern Docs Integration — a renderer with a built-in HTTP client.
- Swagger UI Customization — customizing the classic interactive renderer.