Qubik's Site

Customization

How far you can take the theme without forking its intent: colors and modes, fonts and icons, your own page views, the custom pre-footer section, friend-link data, and the styling rules to work within.

Color

One color configures the theme.

ts
ts
mainColor: "#80E0A7",

Every auxiliary tone — hover, dimmed text, subtle backgrounds, borders, selection — is derived from it in SCSS. That is a hard rule, not a convenience: design/color-system.md §3. If you need a variant, compute it from --ct-main; don't add a second configurable color.

The value is written into <head> at build time as the --ct-main custom property, so the first paint is already your color.

Body text stays neutral in every mode; the main color is for emphasis and interaction — links, bold, headings, inline code, buttons, active states, selection.

The three color modes

ModeForDefault
darkThe default terminal look
lightBright environments
paperReading and printing — no explorer, no TOC, reduced chromeAlso applied automatically via @media print

Readers cycle them from the tool bar; the choice is remembered and restored before first paint. The status bar shows the active mode as an indicator only. Base palettes come from IBM Carbon, and code blocks use Oxocarbon — dark and light for the matching modes, a lighter variant for paper.

Fonts and icons

Typefaces and icon fonts load as stylesheets injected into <head> — never as npm packages. The one place they are declared is .vitepress/theme/head.ts:

ResourceSourceUsed for
IBM Plex Sans / Serif / MonoGoogle Fonts CSSAll UI and content text
IBM Plex MathjsDelivr (@ibm/plex-math)LaTeX (MathML) and Typst math
Font Awesome FreecdnjsGeneral-purpose icons
Nerd Font SymbolsjsDelivr (ryanoasis/nerd-fonts)TUI chrome only

To self-host, change those URLs (and, if you self-host the Nerd Font, keep the family name the runtime check expects). To change the font stack, edit styles/_fonts.scss. Readers can additionally pick a content font family and size in the settings window; that preference layers on top of your defaults.

The icon split is deliberate: Font Awesome for general icons anywhere, Nerd Font strictly for TUI chrome (tool bar, status bar, explorer, callouts). Nerd Font glyphs are gated behind a font-loaded check with a tofu-safe fallback, so a blocked CDN degrades to plain markers instead of boxes. Rationale: design/typography-and-icons.md.

Styling rules

Three rules keep the theme coherent — follow them in anything you add:

  1. SCSS only. All styling lives in .scss partials under .vitepress/theme/styles/, one per concern, imported by main.scss. Never a <style> block in a Vue SFC, never a plain .css file.
  2. Derive colors, never hardcode a variant of the main color.
  3. Mobile is first-class. Every interactive control is a ≥44 px touch target below 640 px, and no layout may overflow at 360 px.

To restyle an existing surface, edit its partial (_toolbar.scss, _explorer.scss, _posts.scss, …). To style something new, add a partial and @use it from main.scss.

Authored page views

Pages that are mostly components rather than prose — About, Projects, a portfolio — are written as Vue views under .vitepress/theme/views/, and imported by a thin Markdown page. The @ alias points at .vitepress/theme:

md
md
---
title: About
---

<script setup>
import About from "@/views/About.vue";
</script>

<About />

The shipped views/About.vue and views/Projects.vue are language dispatchers: they render views/about/en.vue or views/about/zh-Hans.vue for the active UI language (matched by primary subtag, falling back to English), so switching language re-renders in place. Single-language sites can skip the dispatcher and author one component.

Inside a view, use the theme's Card and the shared card grid — a six-column track with span modifiers you can mix per row:

vue
vue
<ul class="ct-cardgrid">
  <li class="ct-cardgrid__item ct-cardgrid__item--full">…</li>
  <li class="ct-cardgrid__item ct-cardgrid__item--two-thirds">…</li>
  <li class="ct-cardgrid__item ct-cardgrid__item--third">…</li>
</ul>

Modifiers: --third, --half, --two-thirds, --full. Below 640 px the grid collapses to a single column. Card width and the shell prompt are independent — any card can have either.

The full pattern, and why views live in the theme rather than in src/, is in design/content-architecture.md §8.

The strip directly above the footer is a named layout slot you fill by wrapping the theme's Layout:

vue
vue
<!-- .vitepress/theme/SiteLayout.vue -->
<script setup lang="ts">
import Layout from './Layout.vue'
import MySection from './components/MySection.vue'
</script>

<template>
  <Layout>
    <template #pre-footer>
      <MySection />
    </template>
  </Layout>
</template>

Then register the wrapper as the theme's Layout in .vitepress/theme/index.ts (import Layout.vue directly, as above, rather than the Layout re-export from index.ts — a wrapper living inside the theme would otherwise import the module that imports it). Unfilled, the slot renders nothing — and when it is filled, the footer automatically grows the subtler inner separator between your section and the standard rows. The demo ships exactly this pattern as DemoLayout.vue + PreFooterDemo.vue; replace or remove both.

The friends page reads data modules, not configuration: every linksData.mjs under .vitepress/theme/assets/ (at any depth) is picked up at build time, so a generator repository can be added as a git submodule and hand-authored entries can sit beside it.

js
js
const linksData = [
  {
    group: "friends",
    groupName: { en: "Friends", "zh-Hans": "朋友们" },
    groupDesc: { en: "The people behind the links." },
    entries: [
      {
        title: "Qubik's Website",
        url: "https://qubik.top",
        description: { en: "The theme author's corner of the web." },
        avatar: "https://github.com/Qubik65536.png",
      },
    ],
  },
];

export default linksData;

Sources merge by group id: the first occurrence fixes the group's position, labels come from the first source that provides them, and later sources append their entries. Generated data uses plain strings; hand-authored files may use per-language maps, and themeConfig.friends.groups can relabel a generated group without touching the data. Entries without an avatar get a placeholder glyph, and a broken avatar URL falls back to it too.

Format details and the generator workflow: design/friend-links.md.

Favicon and site mark

The favicon is src/public/favicon.svg, referenced from head.ts. Replace the file, or point the <link> at your own asset.

One constraint on the chrome itself: no third-party branding in the tool bar, status bar, or window frames — no VitePress or Vue logos, no framework wordmarks. The footer's "Powered by" line is the place for attribution. This is a binding identity rule (design/design-language.md §3), and the reason the theme ships its own mark rather than borrowing one.

Going further

Anything beyond this means editing theme internals — components under .vitepress/theme/components/, page types under pages/, behavior under composables/. That is supported (it's your copy of the theme), but read the design documents first: they are binding for the parts of the UI they cover, and following them is what keeps a modified theme coherent.

READ~/docs/guide/customization
TOPdark