Migrating from Swagger UI to Scalar for Better DX

Swapping Swagger UI for Scalar is rarely just a script-tag change — there is custom CSS to audit, a spec to validate, a CI step to replace, and legacy URLs that other teams have bookmarked. This guide is part of Scalar & Modern Docs Integration and the broader Developer Portal Frameworks & UI Setup section. It lays out an ordered migration: validate, build, embed, redirect, and cut over without breaking downstream imports.

The migration is worth doing when Swagger UI’s interactive console and theming have become a maintenance burden, or when you want the cleaner request client and scoped theming Scalar provides — but only once the spec is genuinely OpenAPI 3.x, since Scalar rejects 2.0 outright.

Swagger UI to Scalar migration Validate the spec, build with the Scalar CLI, embed Scalar, then redirect legacy Swagger UI URLs to the new docs. Swagger UI legacy validate spectral lint scalar build embed Scalar at /docs 301 from legacy

Problem & Context

The before-state: Swagger UI is wired up with a pile of .swagger-ui-scoped CSS overrides, a build step that copies swagger-ui-dist assets, and a spec that may be Swagger 2.0 in disguise — Swagger UI renders malformed 2.0 specs quietly, so nobody noticed. Other teams have hard-coded links to /swagger-ui.html and /api-docs in runbooks and CI dashboards. A naive swap to Scalar breaks all of that: the CSS does nothing because Scalar uses scoped CSS variables, the build fails on an unconverted 2.0 spec, and the old URLs 404.

A clean migration validates first, swaps the build, embeds Scalar, and only then retires the legacy routes with redirects rather than deletions.

Step-by-Step Solution

1. Validate and audit the spec

Scalar requires OpenAPI 3.0.3 or 3.1.0. Install the validators and run them:

npm install -g @apidevtools/swagger-parser @stoplight/spectral-cli
npx swagger-parser validate openapi.yaml
npx spectral lint openapi.yaml --ruleset .spectral.yaml

Expected output for a clean spec:

openapi.yaml is valid
0 errors, 0 warnings

If the spec is Swagger 2.0, convert it, then review by hand:

npx swagger2openapi swagger.yaml --outfile openapi.yaml

swagger2openapi does not upgrade x- extensions or move securityDefinitions into components.securitySchemes — fix those manually before continuing.

2. Swap the CI build step

Replace the Swagger UI asset copy with the Scalar CLI. Note that @scalar/cli has no validate subcommand, so keep Spectral as the gate:

# .github/workflows/docs.yml
name: Build API Docs
on:
  push:
    paths: ['openapi.yaml']
jobs:
  validate-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Validate spec
        run: npx @stoplight/spectral-cli lint openapi.yaml --ruleset .spectral.yaml
      - name: Build Scalar docs
        run: npx @scalar/cli build --input openapi.yaml --output ./dist/docs

3. Embed Scalar

Install the reference component and mount it. Pin the version in package.json, not in the import path:

npm install @scalar/api-reference
// App.vue (Vue 3)
import { ApiReference } from '@scalar/api-reference'

export default {
  components: { ApiReference },
  template: `
    <ApiReference
      :configuration="{
        spec: { url: '/openapi.yaml' },
        theme: 'default',
        darkMode: false,
        hideDownloadButton: false
      }"
    />
  `
}

For a no-framework deployment, use the CDN script tag instead:

<script
  id="api-reference"
  data-url="/openapi.yaml"
  src="https://cdn.jsdelivr.net/npm/@scalar/api-reference/dist/browser/standalone.js"
></script>

4. Redirect legacy URLs

Point old Swagger UI paths at the new docs with 301s, and return 410 for routes that no longer exist:

# nginx.conf
location = /swagger-ui.html { return 301 /docs; }
location = /api-docs        { return 301 /docs; }
location /swagger-resources { return 410; }

5. Cut over and purge caches

After deploying, purge CDN edge caches so readers do not hit stale routing. For Cloudflare:

curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
  -H "Authorization: Bearer ${CF_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything":true}'

Watch 404 rates in analytics for 48 hours to catch any redirect gaps.

Complete Working Example

A single standalone index.html that serves Scalar from the CDN with dark mode and a custom accent, ready to drop behind your web server while you migrate:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Acme API Reference</title>
    <style>
      /* Scalar uses scoped CSS variables, not .swagger-ui selectors */
      :root {
        --scalar-color-accent: #0052cc;
        --scalar-font: 'Inter', system-ui, sans-serif;
      }
      body { margin: 0; }
    </style>
  </head>
  <body>
    <script
      id="api-reference"
      data-url="/openapi.yaml"
      data-configuration='{"theme":"default","darkMode":true}'
      src="https://cdn.jsdelivr.net/npm/@scalar/api-reference/dist/browser/standalone.js"
    ></script>
  </body>
</html>

Pair this with the Nginx redirects above and the Spectral-gated CI build, and the migration is reproducible from a single commit.

Gotchas & Edge Cases

  • Custom Swagger UI CSS stops working. Scalar exposes scoped CSS variables such as --scalar-color-accent, not .swagger-ui class selectors. Re-express your brand colors and fonts as Scalar variables rather than porting the old stylesheet.
  • Interactive client returns CORS 403. Scalar runs requests from the browser, so the target server must allow the docs origin. Add Access-Control-Allow-Origin: https://docs.yourdomain.com at the API gateway, or proxy test calls through a same-origin endpoint.
  • @scalar/cli build fails on missing peers. The CLI needs its peer dependencies installed. Run npm ci before scalar build so the lockfile tree is present in the runner.

The specification is the part that transfers unchanged. Both renderers read the same OpenAPI document, so a migration between them touches the mount, the theming, and any customization you had built — never the contract. That is worth stating explicitly because it bounds the work: if a migration proposal involves editing the specification, something has been misunderstood.

What does not transfer is every customization you accumulated. CSS written against one renderer’s generated class names is worthless against another’s, plugins are renderer-specific by definition, and configuration options do not map one to one. Inventory those before estimating, because a deployment with three configuration options is an afternoon and one with a stylesheet full of class-name overrides and four plugins is a project.

Renderer migration: what moves The OpenAPI document transfers unchanged while renderer-specific styling, plugins and configuration must be rebuilt. transfers unchanged the OpenAPI document examples, security schemes, tags no contract edits required must be rebuilt class-name CSS · plugins config options · deep links scales with what you customised

Deep links into the reference will change. Each renderer derives its fragment identifiers differently, so every bookmark, ticket reference, and cross-link into a specific operation breaks at the cutover. Collect the operation URLs that actually receive traffic, map them to the new scheme, and ship redirects with the migration — this is the same discipline a platform migration needs, at a smaller scale.

Operation deep links change Fragment identifiers differ between renderers, so existing links to specific operations need mapping and redirecting at the cutover. old: #operation/listInvoices in tickets and bookmarks redirect map new fragment scheme old links keep working

FAQ

Can I run Scalar alongside Swagger UI during migration?

Yes. Mount each on a separate path — for example /docs for Scalar and /docs-legacy for Swagger UI — and route a percentage of traffic with a feature flag or A/B split. That lets you compare developer experience on real usage before retiring Swagger UI for good.

Does Scalar support OpenAPI 2.0 specs?

No. Scalar requires OpenAPI 3.0.3 or 3.1.0 and rejects 2.0 definitions at build time. Convert with swagger2openapi first, then review extensions and security definitions by hand because the converter does not normalize them.

Will migrating break existing Postman or Insomnia imports?

No. Those tools import the raw OpenAPI YAML or JSON, not the rendered UI. As long as the spec URL stays reachable and the document is valid OpenAPI 3.x, imports continue to work exactly as before.

Run both renderers in parallel before switching. A renderer swap is unusually cheap to trial because the input is unchanged: publish the new reference at a second path, point a handful of people at it, and compare. Anything that renders differently — a schema that displays oddly, an example that is formatted unexpectedly, an authentication scheme that presents in an unfamiliar way — surfaces immediately and costs nothing to discover, because the existing reference is still live and authoritative throughout.

Keep the parallel period long enough to cover a spec change. Most renderer differences appear not on the specification you tested with but on the next one, when a construct you had not used before renders in a way you did not expect. A fortnight that includes at least one contract release tells you far more than a careful afternoon of comparison.

Verify the console separately from the page. The reference and the request console fail independently, and the console is the part with a live dependency: a renderer swap changes how requests are constructed and where credentials are attached, so a console that worked before can fail afterwards for reasons that have nothing to do with rendering. Exercise it against a real environment before the cutover rather than assuming it carried over.

Plan the cutover as a redirect exercise with a rendering change attached, rather than the other way round. The rendering improvement is what motivated the work and it will be visible on day one; the links are what readers and search engines have accumulated over years, and they are the part that is expensive to get wrong and impossible to fix retroactively.

Keeping the old reference live throughout the parallel period costs nothing and gives you a working fallback for the entire migration, which is the cheapest insurance available on a change of this kind.

Decommission the old path only once the redirects have been serving traffic long enough for the numbers to show that nothing is still asking for the original URLs.