Skip to content

UI Components & Custom Development

The UI of EpoCanvas Docs is not built from scratch. Instead, it performs targeted overrides on top of Starlight’s native components: Starlight’s page skeleton and content processing are kept, while display components such as the top bar, sidebar, table of contents, and search are replaced to achieve the desired three-column layout and interactions. This page explains the structure of this component system and how to modify it.


The Component Override Mechanism

Starlight lets you replace any native component with a custom implementation via the components field in astro.config.mjs. This project overrides 7 components:

// astro.config.mjs (excerpt)
components: {
Header: './src/components/starlight/Header.astro',
Sidebar: './src/components/starlight/Sidebar.astro',
TableOfContents: './src/components/starlight/TableOfContents.astro',
PageTitle: './src/components/starlight/PageTitle.astro',
TwoColumnContent: './src/components/starlight/TwoColumnContent.astro',
Search: './src/components/starlight/Search.astro',
Pagination: './src/components/starlight/Pagination.astro',
},

At build time, every position where Starlight renders a page uses the file specified here first. Components that are not overridden (such as the footer or the mobile menu) continue to use the native implementations.


Responsibilities of the Seven Custom Components

All source code lives in src/components/starlight/. Their sizes and responsibilities are as follows:

Component fileSizeResponsibility
Header.astroAbout 646 linesEverything in the top bar: logo, search box, main navigation, version badge, language switcher, theme toggle, GitHub and Telegram links
Search.astroAbout 837 linesDual-mode search: in-page search from the top bar (highlighting and counters) + the Ctrl+K site-wide search dialog (Pagefind UI)
Pagination.astroAbout 123 linesBottom “previous / next” cards: flat thin borders, theme-colored titles, and ↙/↘ diagonal arrows indicating the paging direction
TwoColumnContent.astroAbout 77 linesTwo-column skeleton for body text and the right-side table of contents; controls the fixed width and scrolling of the right column
TableOfContents.astroAbout 64 linesThe “On this page” title, icon, and TOC list; filters out the page’s own title
PageTitle.astroAbout 62 linesThe page’s main title (taken from the Frontmatter title) and the “Last updated” timestamp
Sidebar.astroAbout 22 linesA thin wrapper that reuses Starlight’s native SidebarPersister to keep the sidebar scroll position across page changes

Data Flow: Three Configuration Files Drive the Entire UI

The custom components contain no business data themselves. The UI content is driven by three configuration files:

astro.config.mjs ──→ locales + sidebar arrays ──→ Sidebar.astro renders the left sidebar (each language gets its translated labels)
src/config/navigation.ts ──→ navigationConfig ──→ Header.astro renders the top navigation and highlights (links get the language prefix automatically)
src/utils/i18n.ts ──→ UI_TRANSLATIONS dictionary ──→ components fetch entries for the current language at build time
  • The left sidebar only reads the sidebar declarations in astro.config.mjs; new documents must be registered there. Each entry’s translations field provides menu text in 10 languages;
  • The top navigation fetches each item’s display text through its labelKey from the i18n.ts dictionary, and the match function decides which button is highlighted for the current page (the language prefix is stripped before matching);
  • UI copy (search box placeholders, the “On this page” title, theme toggle tooltips, etc.) is output directly in the current language at build time by each component calling getTranslation(key, lang). There is no runtime replacement script in the page.

In other words: to change UI content, look for the corresponding configuration file first. Only changing appearance (spacing, colors, icons) requires touching component source code.


Key Implementation Details of Each Component

PageTitle: Page Title and Real Update Times

The page’s main title is read directly from the Frontmatter title, so do not write a # level-one heading in the body. The “Last updated” timestamp comes from the Git commit history at build time (lastUpdated: true is enabled in astro.config.mjs). It refreshes automatically with every commit and requires no manual maintenance.

Sidebar.astro is only about 20 lines long; its core is reusing Starlight’s official SidebarPersister component, which keeps the sidebar DOM from being rebuilt across page changes, preserving the scroll position. This is how the left sidebar “does not jump” between pages.

TableOfContents: Generating the On-Page TOC

The TOC data is generated by Starlight at build time from the body headings (## and ###); the component only filters out the page title itself and renders the list. Scroll highlighting is handled in the browser by the starlight-toc custom element, with no framework dependency.

TwoColumnContent: The Single Source of Truth for the Right Column Width

The right TOC column is fixed at 20rem under @media (min-width: 72rem) (21rem on ultra-wide screens at 90rem and above), and the maximum body text width subtracts the right column’s width accordingly. To adjust the right column’s width, change this one file only; do not scatter overrides across other stylesheets.

Header: Navigation, Theme, and Language

  • Navigation buttons are rendered by iterating over navigationConfig; the active state styling is decided by the match function’s return value, and links get the current language prefix automatically via localizedHref();
  • The theme toggle writes to the starlight-theme key in LocalStorage; on page load the initial theme is decided in the order “local choice → system preference”;
  • Each item in the language dropdown is a real link to the current page’s version in that language; clicking navigates immediately, with no extra state storage;
  • Both the GitHub and the Telegram icons on the right of the top bar are hardcoded in src/components/starlight/Header.astro, so changing an address means editing that file; Telegram points at the @epocanvas account page and its tooltip is the social.telegram string. The social entry in astro.config.mjs drives the separate icon set at the bottom of the sidebar, and the two do not affect each other.

One component implements two search modes (see Full-Text Search & Keyboard Shortcuts for details):

  1. In-page search: the top bar input box; pressing Enter jumps between matches on the current page, with highlighting done by script-based markers;
  2. Site-wide search: a <dialog> popup with Pagefind’s default UI; the index is generated during pnpm run build.

Pagination: Page Cards

The previous/next page data is computed by Starlight at build time from the sidebar order (Astro.locals.starlightRoute.pagination); the component only renders it: two equal-width cards with thin borders and no shadows, theme-colored titles, and ↙ / ↘ diagonal arrows that shift along the paging direction on hover. The arrows are inline SVG paths and mirror automatically if the site is used for RTL languages.


Customization Notes

  • Prefer CSS variables for style changes: colors, fonts, and layout dimensions are centralized in the :root variables of src/styles/custom.css, see Site Configuration & Style Customization. Most customization does not require touching components;
  • Only touch components to change interactions: when adding buttons or adjusting structure, fetch UI text with getTranslation(key, lang) and add the entries for all 10 languages in i18n.ts. Languages left out fall back to Chinese;
  • Always verify locally after changes: use pnpm run dev to check interactions, and pnpm run build to confirm types and the build pass (for local commands, see FAQ & Troubleshooting).

For common concrete customization tasks, see Common Customization Recipes directly.