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
| Layer | Choice |
|---|---|
| Site framework | Docusaurus 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 */
}
5. Swizzle the Footer
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
footerblock fromthemeConfig(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-componentslibrary 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.
Adding the provincial Header and Footer (BC Gov hosting)
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.mdan explicitsidebar_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 rendersHome > Conceptual Framework > ...instead ofHome > Docs > Conceptual Framework > .... Fixing this means swizzlingDocBreadcrumbsto prepend a section-root crumb keyed offuseActivePlugin().pluginId(seesrc/theme/DocBreadcrumbs/index.tsxin 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.
Adding local search
@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 withnpm run build && npm run serve. - If you reuse
@theme/SearchBaroutside the navbar (e.g. a big homepage search box), be aware that the underlyingautocomplete.jslibrary 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 inputwidth: 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) towidth: 100%; display: block;. - Infima's built-in search icon uses
fill="currentColor"inside an inlined SVGbackground-image.currentColordoesn't inherit through abackground-imagethe 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;
}