Skip to main content

How We Built This Docs Site

A high-level guide for anyone wanting to use this same approach — a Docusaurus v3 site styled with the BC Government design system — for a BC Gov or BC Gov-adjacent documentation project.


Stack

LayerChoice
Site frameworkDocusaurus v3 (TypeScript, classic preset)
Font@bcgov/bc-sans — BC Sans
Design tokens@bcgov/design-tokens — CSS custom properties
Component library@bcgov/design-system-react-components v0.8+
Hosting (planned)GitHub Pages / OpenShift / Cloudflare Pages

Steps

1. Scaffold a Docusaurus site

npx create-docusaurus@latest my-docs classic --typescript
cd my-docs

2. Install BC Gov packages

npm install @bcgov/bc-sans @bcgov/design-tokens @bcgov/design-system-react-components

3. Import the font and design tokens

In src/css/custom.css, add at the top:

@import '@bcgov/bc-sans/css/BCSans.css';
@import '@bcgov/design-tokens/css/variables.css';

4. Map Infima CSS variables to BC Gov colours

Docusaurus uses Infima for its design system. Override the primary colour scale in src/css/custom.css to match BC Gov blue (#013366):

:root {
--ifm-color-primary: #013366;
--ifm-color-primary-dark: #012e5c;
/* ... full scale ... */
--ifm-font-family-base: 'BCSans', sans-serif;
--ifm-navbar-background-color: #013366;
--ifm-navbar-link-color: #ffffff;
--ifm-navbar-link-hover-color: #fcba19; /* BC Gov gold */
}

Create src/theme/Footer/index.tsx to replace the default Docusaurus footer with a minimal custom one (or a full BC Gov <Footer> component if hosting under BC Gov).

# Optional — use the docusaurus swizzle CLI to start from the original source:
npx docusaurus swizzle @docusaurus/theme-classic Footer --wrap --typescript

6. Update docusaurus.config.ts

  • Set title, tagline, url, organizationName, projectName
  • Remove the default footer block from themeConfig (your swizzle handles it)
  • Trim the navbar to only your real nav items

7. Customise the homepage

Edit src/pages/index.tsx and src/components/HomepageFeatures/index.tsx with your project's title, description, and feature cards.


Notes on the BC Gov component library

  • The @bcgov/design-system-react-components library ships no CSS bundle — styles are driven entirely by the CSS custom properties from @bcgov/design-tokens. Import the tokens before using any components.
  • The library is React-only (built on React Aria). No Vue/Svelte/web-components equivalent exists as of v0.8.0.
  • Available navigation components: Header, Footer, Subheader, Menu. These are ideal if your site is actually hosted by BC Gov and needs the provincial wordmark and territory acknowledgement.

When the site is hosted under a BC Gov domain, add the provincial identity bar:

npx docusaurus swizzle @docusaurus/theme-classic Layout --wrap --typescript

Then in src/theme/Layout/index.tsx:

import {Header, Footer} from '@bcgov/design-system-react-components';

export default function Layout({children, ...props}) {
return (
<>
<Header title="Your Site Name" />
<OriginalLayout {...props}>{children}</OriginalLayout>
</>
);
}

And replace the Footer swizzle with the BC Gov <Footer> component (which includes the land acknowledgement by default).


Adding multiple independent doc sections (e.g. Case Studies, Model Garden)

If you want more than one top-level nav section with its own sidebar — not nested under your main Docs sidebar — add extra instances of @docusaurus/plugin-content-docs in docusaurus.config.ts:

plugins: [
[
'@docusaurus/plugin-content-docs',
{
id: 'case-studies',
path: 'case-studies',
routeBasePath: 'case-studies',
sidebarPath: './sidebars-case-studies.ts',
},
],
],

Then reference it in the navbar with type: 'docSidebar', docsPluginId: 'case-studies', and a matching sidebarId.

Gotchas with this pattern

  • Give each section's root index.md an explicit sidebar_position: 0. Docusaurus's autogenerated sidebar sorts items with an explicit position before items without one. If your section's landing page has no position but its category folders do (position: 1, 2, 3…), the navbar link resolves to the first category instead of the root page.
  • Breadcrumbs won't show the section root by default. Each plugin instance's own landing page isn't a node in its own sidebar tree, so Docusaurus's useSidebarBreadcrumbs() has no way to know it exists — a deep page renders Home > Conceptual Framework > ... instead of Home > Docs > Conceptual Framework > .... Fixing this means swizzling DocBreadcrumbs to prepend a section-root crumb keyed off useActivePlugin().pluginId (see src/theme/DocBreadcrumbs/index.tsx in this repo for a working example) — and skip that injected crumb when it would duplicate the page's own (only) breadcrumb entry on the root page itself.
  • When swizzling a file that's a copy of Docusaurus's own source (as opposed to a fresh wrapper), keep its original MIT copyright header — it's a modified copy of licensed code, not a from-scratch file.

@easyops-cn/docusaurus-search-local is a good fit if you want search with no third-party service, no signup, and no API keys — unlike Algolia DocSearch, it builds its entire index locally at docusaurus build time and ships it as a static JSON file.

themes: [
[
'@easyops-cn/docusaurus-search-local',
{
hashed: true,
indexDocs: true,
indexPages: true,
docsRouteBasePath: ['docs', 'case-studies', 'model-garden'], // one per docs plugin instance
},
],
],

Gotchas

  • The index only exists in production builds. In npm start (dev mode) it shows a placeholder message instead of searching. Test with npm run build && npm run serve.
  • If you reuse @theme/SearchBar outside the navbar (e.g. a big homepage search box), be aware that the underlying autocomplete.js library wraps the raw <input> in a new <span> the first time it's focused, to host the results dropdown. That wrapper has no width of its own — if your CSS gives the input width: 100%, it can suddenly resolve against an unconstrained wrapper and collapse to the browser's default text-input size on focus. Fix by forcing that wrapper span (and only that span — not its siblings, which include an absolutely-positioned clear button and keyboard-shortcut hint that break if you stretch them too) to width: 100%; display: block;.
  • Infima's built-in search icon uses fill="currentColor" inside an inlined SVG background-image. currentColor doesn't inherit through a background-image the way it does for a real inline <svg> element, so the icon renders black regardless of theme — invisible against a dark input in dark mode. If you restyle the search input, you'll likely need to supply your own icon SVG (with a hardcoded fill) for dark mode.

BC Gov navbar colour is theme-independent — plan for it

If (like this site) you hardcode the navbar to BC Gov dark blue regardless of light/dark mode, remember that anything relying on color: inherit (e.g. Infima's .clean-btn, used by the light/dark mode toggle and the mobile nav hamburger) will inherit the page's default text color, not the navbar's white link color. That's invisible in light mode against a dark navbar. Force it explicitly:

.navbar .clean-btn {
color: #ffffff;
}