
Rspress Custom Theme
- 241 installs
- 86 repo stars
- Updated August 4, 2026
- rstackjs/agent-skills
Use rspress-custom-theme for development tasks
About
rspress-custom-theme: A skill for development. This provides functionality for development workflows.
- rspress-custom-theme
Rspress Custom Theme by the numbers
- 241 all-time installs (skills.sh)
- +9 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #1,564 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rstackjs/agent-skills --skill rspress-custom-themeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 241 |
|---|---|
| repo stars | ★ 86 |
| Last updated | August 4, 2026 |
| Repository | rstackjs/agent-skills ↗ |
What it does
Use rspress-custom-theme for development tasks
Files
Rspress Custom Theme
Guide for customizing Rspress (v2) themes. Rspress offers four levels of customization, from lightest to heaviest. Always prefer the lightest approach that meets the requirement — lighter approaches are more maintainable and survive Rspress upgrades.
Workflow
1. Understand the user's goal — what do they want to change? (colors, layout, inject content, replace a component entirely?) 2. Pick the right level using the decision flow below 3. Set up `theme/index.tsx` if needed (Levels 1A, 3, 4 all need it) 4. Implement following the patterns in this skill and reference files 5. Verify the user's Rspress version is v2 (imports use @rspress/core/* not rspress/*)
Decision Flow
| User wants to... | Level | Approach |
|---|---|---|
| Change brand colors, fonts, spacing, shadows | 1 | CSS variables |
| Adjust a specific component's style (borders, padding, etc.) | 2 | BEM class overrides |
| Add content around existing components (banners, footers, logos) | 3 | Layout slots (wrap) |
Override MDX rendering (custom <h1>, <code>, etc.) | 3 | components slot |
| Wrap the app in a provider (state, analytics, auth) | 4 | Eject Root |
| Replace built-in icons (logo, GitHub, search, etc.) | — | Icon re-export |
| Completely replace a built-in component | 4 | Eject that component |
| Add a global floating component (back-to-top, chat widget) | — | globalUIComponents config |
| Control page layout structure (hide sidebar, blank page) | — | Frontmatter pageType |
---
theme/index.tsx — The Entry Point
Levels 1A, 3, and 4 all require a theme/index.tsx file in the project root (sibling to docs/). This is the single entry point for all theme customizations:
project/
├── docs/
├── theme/
│ ├── index.tsx # Theme entry — re-exports + overrides
│ ├── index.css # CSS variable / BEM overrides (optional)
│ └── components/ # Ejected components (Level 4)
└── rspress.config.tsMinimal setup:
// theme/index.tsx
import './index.css'; // optional
export * from '@rspress/core/theme-original';Critical import rule: Inside theme/ files, always import from @rspress/core/theme-original. The path @rspress/core/theme resolves to your own theme/index.tsx, which causes circular imports. (In docs/ MDX files, @rspress/core/theme is fine — it correctly points to your custom theme.)
---
Level 1: CSS Variables
Override CSS custom properties for brand colors, backgrounds, text, code blocks, and more.
Option A — theme/index.css (use when you also have component overrides in theme/index.tsx):
/* theme/index.css */
:root {
--rp-c-brand: #7c3aed;
--rp-c-brand-light: #8b5cf6;
--rp-c-brand-dark: #6d28d9;
}
.dark {
--rp-c-brand: #a78bfa;
}Option B — globalStyles (use when you only need CSS changes, no component overrides):
// rspress.config.ts
export default defineConfig({
globalStyles: path.join(__dirname, 'styles/custom.css'),
});Full variable list: Read references/css-variables.md for all available CSS variables with light/dark defaults.---
Level 2: BEM Class Overrides
All built-in components follow BEM naming: .rp-[component]__[element]--[modifier].
Common targets: .rp-nav, .rp-link, .rp-tabs, .rp-codeblock, .rp-codeblock__title, .rp-nav-menu__item--active.
Use these in your CSS file for targeted style changes when CSS variables aren't granular enough.
---
Level 3: Wrap (Layout Slots)
Inject content at specific positions in the layout without replacing built-in components. Override Layout in theme/index.tsx:
// theme/index.tsx
import { Layout as OriginalLayout } from '@rspress/core/theme-original';
export * from '@rspress/core/theme-original';
export function Layout() {
return (
<OriginalLayout beforeNavTitle={<MyLogo />} bottom={<CustomFooter />} />
);
}Use runtime hooks inside slot components — import from @rspress/core/runtime: useDark(), useLang(), useVersion(), usePage(), useSite(), useFrontmatter(), useI18n().
All slots & examples: Read references/layout-slots.md for the complete slot list and usage patterns including i18n and MDX component overrides.---
Level 4: Eject
Copy a built-in component's source for full replacement. Only use when wrap/slots cannot achieve the customization.
rspress eject # list available components
rspress eject DocFooter # eject to theme/components/DocFooter/Then re-export in theme/index.tsx (named export takes precedence over the wildcard):
export * from '@rspress/core/theme-original';
export { DocFooter } from './components/DocFooter';Component list & patterns: Read references/eject-components.md for available components, workflow, and common patterns.---
Custom Icons
Rspress has 27 built-in icons used across the UI. You can replace any of them by re-exporting your own icon component with the same name — no ejection needed. This uses the same theme/index.tsx mechanism: your named export takes precedence over the wildcard re-export.
Icon type: Each icon is a React component or a URL string:
import type { FC, SVGProps } from 'react';
type Icon = FC<SVGProps<SVGSVGElement>> | string;Example 1 — Replace an icon with a custom SVG component:
// theme/index.tsx
export * from '@rspress/core/theme-original';
// Named export overrides the wildcard — replaces the GitHub icon site-wide
export const IconGithub = (props: React.SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 ..." fill="currentColor" />
</svg>
);Example 2 — Use an SVGR import:
// theme/index.tsx
export * from '@rspress/core/theme-original';
import CustomGithubIcon from './icons/github.svg?react';
export const IconGithub = CustomGithubIcon;Using `SvgWrapper` in MDX or custom components:
import { SvgWrapper, IconGithub } from '@rspress/core/theme';
<SvgWrapper icon={IconGithub} width={24} height={24} />Available icons: IconArrowDown, IconArrowRight, IconClose, IconCopy, IconDeprecated, IconDown, IconEdit, IconEmpty, IconExperimental, IconExternalLink, IconFile, IconGithub, IconGitlab, IconHeader, IconJump, IconLink, IconLoading, IconMenu, IconMoon, IconScrollToTop, IconSearch, IconSmallMenu, IconSuccess, IconSun, IconTitle, IconWrap, IconWrapped.
Source: See the icons source for default implementations.
---
Global UI Components
For components that should render on every page without theme overrides:
// rspress.config.ts
export default defineConfig({
globalUIComponents: [
path.join(__dirname, 'components', 'BackToTop.tsx'),
[
path.join(__dirname, 'components', 'Analytics.tsx'),
{ trackingId: '...' },
],
],
});---
Page Types
Control layout per page via frontmatter pageType:
| Value | Description |
|---|---|
home | Home page with navbar |
doc | Standard doc with sidebar and outline |
doc-wide | Doc without sidebar/outline |
custom | Custom content with navbar only |
blank | Custom content without navbar |
404 | 404 error page |
Fine-grained: set navbar: false, sidebar: false, outline: false, footer: false individually.
---
Common Pitfalls
- Circular import: Using
@rspress/core/themeinstead of@rspress/core/theme-originalintheme/files — causes infinite loop. - Eject over-use: Ejecting when a Layout slot or CSS variable would suffice — creates upgrade burden.
- Missing re-export: Forgetting
export * from '@rspress/core/theme-original'intheme/index.tsx— breaks all un-overridden components. - v1 imports: Using
rspress/themeor@rspress/theme-default— these are v1 paths. v2 uses@rspress/core/theme-original.
Reference
- Custom theme guide: <https://rspress.rs/guide/basic/custom-theme>
- CSS variables: <https://rspress.rs/ui/vars>
- Layout component: <https://rspress.rs/ui/layout-components/layout>
- Built-in icons: <https://rspress.rs/ui/icons/>
- Built-in hooks: <https://rspress.rs/ui/hooks/>
- CLI commands (eject): <https://rspress.rs/api/commands>
CSS Variables Reference
Complete list of CSS variables exposed by Rspress for theme customization.
- Override location:
theme/index.cssorglobalStylesinrspress.config.ts - Dark mode selector:
.dark { ... } - Official docs: <https://rspress.rs/ui/vars>
---
Brand Colors (shared)
:root {
--rp-c-brand: #0095ff;
--rp-c-brand-light: #33adff;
--rp-c-brand-lighter: #c6e0fd;
--rp-c-brand-dark: #0077ff;
--rp-c-brand-darker: #005fcc;
--rp-c-brand-tint: rgba(127, 163, 255, 0.16);
}Base Variables
| Variable | Light | Dark |
|---|---|---|
--rp-c-bg | #ffffff | #121212 |
--rp-c-bg-soft | #f8f8f9 | #292e37 |
--rp-c-bg-mute | #f1f1f1 | #343a46 |
--rp-c-bg-alt | #fff | #000 |
--rp-c-divider | rgba(0, 0, 0, 0.25) | rgba(84, 84, 84, 0.65) |
--rp-c-divider-light | rgba(0, 0, 0, 0.12) | rgba(84, 84, 84, 0.48) |
Text Colors
| Variable | Light | Dark |
|---|---|---|
--rp-c-text-0 | #000000 | #ffffff |
--rp-c-text-1 | #242424 | rgba(255, 255, 245, 0.93) |
--rp-c-text-2 | rgba(0, 0, 0, 0.7) | rgba(255, 255, 245, 0.65) |
--rp-c-text-3 | rgba(60, 60, 60, 0.33) | rgba(235, 235, 235, 0.38) |
--rp-c-text-4 | rgba(60, 60, 60, 0.18) | rgba(235, 235, 235, 0.18) |
--rp-c-link | var(--rp-c-brand-dark) | var(--rp-c-brand-light) |
Inline Code
| Variable | Light | Dark |
|---|---|---|
--rp-c-text-code | #476582 | #c9def1 |
--rp-c-text-code-bg | rgba(153, 161, 179, 0.06) | rgba(255, 255, 255, 0.06) |
--rp-c-text-code-border | rgba(0, 0, 0, 0.035) | rgba(255, 255, 255, 0.04) |
Code Blocks
| Variable | Light | Dark |
|---|---|---|
--rp-code-font-size | 0.875rem | 0.875rem |
--rp-code-title-bg | #f8f8f9 | #191919 |
--rp-code-block-color | rgb(46, 52, 64) | rgb(229, 231, 235) |
--rp-code-block-bg | var(--rp-c-bg) | var(--rp-c-bg) |
--rp-code-block-border | 1px solid var(--rp-c-divider-light) | 1px solid var(--rp-c-divider-light) |
--rp-code-block-shadow | none | none |
Shiki Syntax Highlighting
Rspress uses .dark on html as the public dark-mode toggle for general theme overrides. The Shiki token blocks below target html:not(.rp-dark) and html.rp-dark, which Rspress uses internally for syntax highlighting variables.
Light
html:not(.rp-dark) {
--shiki-foreground: inherit;
--shiki-background: transparent;
--shiki-token-constant: #1976d2;
--shiki-token-string: #31a94d;
--shiki-token-comment: rgb(182, 180, 180);
--shiki-token-keyword: #cf2727;
--shiki-token-parameter: #f59403;
--shiki-token-function: #7041c8;
--shiki-token-string-expression: #218438;
--shiki-token-punctuation: #242323;
--shiki-token-link: #22863a;
--shiki-token-deleted: #d32828;
--shiki-token-inserted: #22863a;
}Dark
html.rp-dark {
--shiki-foreground: inherit;
--shiki-background: transparent;
--shiki-token-constant: #6fb0fa;
--shiki-token-string: #f9a86e;
--shiki-token-comment: #6a727b;
--shiki-token-keyword: #f47481;
--shiki-token-parameter: #ff9800;
--shiki-token-function: #ae8eeb;
--shiki-token-string-expression: #4fb74d;
--shiki-token-punctuation: #bbbbbb;
--shiki-token-link: #f9a76d;
--shiki-token-deleted: #ee6d7a;
--shiki-token-inserted: #36c47f;
}Grays (shared)
:root {
--rp-c-gray: #8e8e8e;
--rp-c-gray-light-1: #aeaeae;
--rp-c-gray-light-2: #c7c7c7;
--rp-c-gray-light-3: #d1d1d1;
--rp-c-gray-light-4: #e5e5e5;
--rp-c-gray-light-5: #f2f2f2;
}Shadows (shared)
:root {
--rp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.02), 0 1px 0 rgba(0, 0, 0, 0.06);
--rp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.07);
--rp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
--rp-shadow-4: 0 14px 44px rgba(0, 0, 0, 0.12), 0 3px 9px rgba(0, 0, 0, 0.12);
--rp-shadow-5:
0 18px 56px rgba(0, 0, 0, 0.16), 0 4px 12px rgba(0, 0, 0, 0.16);
}Radius (shared)
:root {
--rp-radius: 1rem;
--rp-radius-small: 0.5rem;
--rp-radius-large: 1.5rem;
}Home Page
Note: ... in gradient values marks omitted gradient parameters, not literal CSS. See the official docs link above for complete values.
| Variable | Light | Dark |
|---|---|---|
--rp-home-hero-secondary-color | #a673ff | #a673ff |
--rp-home-hero-title-color | transparent | transparent |
--rp-home-hero-title-bg | linear-gradient(90deg, var(--rp-c-brand-dark) 0%, var(--rp-c-brand-dark) 30%, var(--rp-home-hero-secondary-color) 100%) | (same) |
--rp-home-background-bg | radial-gradient(...), radial-gradient(...), radial-gradient(...), #fff | radial-gradient(...), radial-gradient(...), radial-gradient(...), #121212 |
--rp-home-feature-bg | linear-gradient(135deg, #fff, #f9f9f980) | linear-gradient(135deg, #ffffff00, #ffffff08) |
Quick Start
/* Example brand color overrides for the custom theme scaffold. */
/* For more CSS variables, see https://rspress.rs/ui/vars. */
:root {
--rp-c-brand: #ff5e00;
--rp-c-brand-dark: #ff704d;
--rp-c-brand-darker: #ff704d;
--rp-c-brand-light: #ff7524;
--rp-c-brand-lighter: #ff7524;
--rp-c-brand-tint: rgba(255, 94, 0, 0.07);
--rp-home-hero-secondary-color: #ff5e00;
}
.dark {
--rp-c-brand: #ff8c4d;
--rp-home-hero-secondary-color: #ff8c4d;
}Eject Components Reference
Eject copies a built-in component's source code into your project for full customization. This is the heaviest approach — ejected components do not receive automatic updates when Rspress upgrades. Prefer CSS variables, BEM overrides, or Layout slots whenever possible.
Official reference: <https://rspress.rs/api/commands>
---
Eject Command
# List all available components
rspress eject
# Eject a specific component
rspress eject <ComponentName>Ejected source is placed in theme/components/<ComponentName>/.
Available Components
| Component | Description | Consider wrapping first? |
|---|---|---|
Layout | Main layout container with all slot props | Yes — use Layout slots instead |
Root | Application root wrapper | Only eject for global providers |
Banner | Notification banner at top of page | Check top slot first |
NavTitle | Navigation logo and title | Check navTitle / beforeNavTitle slots |
HomeLayout | Complete home page layout | Check home page slots first |
HomeHero | Hero section on home page | Check beforeHero / afterHero slots |
HomeFeature | Feature grid cards | Check beforeFeatures / afterFeatures slots |
HomeBackground | Home page background effects | Try CSS variables first |
HomeFooter | Home page footer | Check bottom slot first |
DocFooter | Documentation page footer | Check beforeDocFooter / afterDocFooter slots |
EditLink | "Edit this page" link | Configure via themeConfig.editLink |
LastUpdated | Last updated timestamp | Usually config is enough |
PrevNextPage | Previous/next page navigation | Check beforeDocFooter slot |
OverviewGroup | Overview page group cards | — |
Tag | Tag/label component | — |
Step-by-Step Eject Workflow
1. Eject the component:
rspress eject DocFooter2. Re-export in theme/index.tsx:
// theme/index.tsx
export * from '@rspress/core/theme-original';
export { DocFooter } from './components/DocFooter';The named export takes precedence over the wildcard re-export, so Rspress uses your custom version.
3. Modify the ejected source in theme/components/DocFooter/.
Common Pattern: Root for Global Providers
The most common eject use case is wrapping the entire app in a context provider (state management, analytics, auth, etc.):
// theme/components/Root/index.tsx
import type { RootProps } from '@rspress/core/theme';
export function Root({ children }: RootProps) {
return (
<ThemeProvider>
<AnalyticsProvider>{children}</AnalyticsProvider>
</ThemeProvider>
);
}// theme/index.tsx
export * from '@rspress/core/theme-original';
export { Root } from './components/Root';Common Pattern: Custom Home Page (HomeLayout)
When the default home page structure (Hero + Features) doesn't meet the design requirements — for example, you need a completely different landing page with custom sections, animations, or a non-standard layout — write a custom HomeLayout component and re-export it directly:
// theme/components/HomeLayout/index.tsx
import { useSite, useLang } from '@rspress/core/runtime';
export function HomeLayout() {
const site = useSite();
const lang = useLang();
const { title, description } = site.siteData;
return (
<div className="custom-home">
<section className="hero">
<h1>{title}</h1>
<p>{description}</p>
<div className="hero-actions">
<a
href={lang === 'zh' ? '/zh/guide/start' : '/guide/start'}
className="primary-btn"
>
Get Started
</a>
<a href="https://github.com/..." className="secondary-btn">
GitHub
</a>
</div>
</section>
<section className="showcase">
{/* Custom content: testimonials, stats, demos, etc. */}
</section>
</div>
);
}// theme/index.tsx
export * from '@rspress/core/theme-original';
export { HomeLayout } from './components/HomeLayout';The named export overrides the built-in HomeLayout from the wildcard re-export — no need to eject first.
If you only need to add content before/after the Hero or Features sections (without replacing the entire home page), prefer Layout slots (beforeHero, afterHero, beforeFeatures, afterFeatures) instead — see references/layout-slots.md.
Common Pattern: Custom Doc Footer
// theme/components/DocFooter/index.tsx
import { useFrontmatter } from '@rspress/core/runtime';
export function DocFooter() {
const frontmatter = useFrontmatter();
return (
<footer className="custom-doc-footer">
{frontmatter.author && <span>Author: {frontmatter.author}</span>}
<a href="https://github.com/...">Edit this page</a>
</footer>
);
}Important Notes
- Always import from
@rspress/core/theme-originalintheme/files, never from@rspress/core/theme(the latter resolves to your owntheme/index.tsx, causing circular imports). - After ejecting, you own that component. Track Rspress changelogs for upstream changes you might want to incorporate manually.
- Run
rspress eject(no args) to see the up-to-date list of available components — the list above may change between Rspress versions.
Layout Slots Reference
The Layout component accepts slot props (React.ReactNode) for injecting content at specific positions without replacing built-in components. This is the recommended way to extend Rspress before considering eject.
Official reference: <https://rspress.rs/ui/layout-components/layout>
---
All Available Slots
Navigation Bar
| Slot | Position |
|---|---|
beforeNav | Before the entire navigation bar |
afterNav | After the entire navigation bar |
beforeNavTitle | Before the nav title/logo (top-left) |
navTitle | Replaces the nav title content |
afterNavTitle | After the nav title/logo |
beforeNavMenu | Before the nav menu items |
afterNavMenu | After the nav menu items |
Sidebar & Outline
| Slot | Position |
|---|---|
beforeSidebar | Above the left sidebar |
afterSidebar | Below the left sidebar |
beforeOutline | Above the right outline (TOC) panel |
afterOutline | Below the right outline panel |
Home Page
| Slot | Position |
|---|---|
beforeHero | Before the Hero section |
afterHero | After the Hero section |
beforeFeatures | Before the Features grid |
afterFeatures | After the Features grid |
Doc Page
| Slot | Position |
|---|---|
beforeDoc | At the very beginning of the doc page |
afterDoc | At the very end of the doc page |
beforeDocContent | Before the document content area |
afterDocContent | After the document content area |
beforeDocFooter | Before the doc footer (prev/next nav) |
afterDocFooter | After the doc footer |
Global
| Slot | Position |
|---|---|
top | At the very top of the entire page |
bottom | At the very bottom of the entire page |
components | Custom MDX component overrides (Record<string, React.ComponentType>) |
---
Usage Pattern
All examples below follow the same structure in theme/index.tsx. The key parts:
- Import
Layoutfrom@rspress/core/theme-original(not@rspress/core/theme— that causes circular imports) - Re-export everything:
export * from '@rspress/core/theme-original' - Export your custom
Layoutthat wraps the original with slot props
Basic — Single Slot
// theme/index.tsx
import { Layout as OriginalLayout } from '@rspress/core/theme-original';
export * from '@rspress/core/theme-original';
export function Layout() {
return <OriginalLayout beforeNavTitle={<MyLogo />} />;
}Multiple Slots
// theme/index.tsx
import { Layout as OriginalLayout } from '@rspress/core/theme-original';
export * from '@rspress/core/theme-original';
export function Layout() {
return (
<OriginalLayout
top={<div className="announcement-bar">New version released!</div>}
bottom={<footer>© 2025 My Company</footer>}
afterOutline={<div>Related resources</div>}
/>
);
}With i18n Hooks
// theme/index.tsx
import { Layout as OriginalLayout } from '@rspress/core/theme-original';
import { useLang } from '@rspress/core/runtime';
export * from '@rspress/core/theme-original';
function LocalizedBanner() {
const lang = useLang();
return <div>{lang === 'zh' ? '欢迎' : 'Welcome'}</div>;
}
export function Layout() {
return <OriginalLayout top={<LocalizedBanner />} />;
}Override MDX Components
The components slot accepts a Record<string, React.ComponentType> to override how MDX elements render:
// theme/index.tsx
import { Layout as OriginalLayout } from '@rspress/core/theme-original';
export * from '@rspress/core/theme-original';
function CustomH1({ children }: { children: React.ReactNode }) {
return (
<h1 style={{ borderBottom: '2px solid var(--rp-c-brand)' }}>{children}</h1>
);
}
export function Layout() {
return <OriginalLayout components={{ h1: CustomH1 }} />;
}---
Available Hooks
Use these hooks inside slot components. Import from @rspress/core/runtime.
| Hook | Purpose |
|---|---|
useDark() | Returns whether dark mode is active |
useLang() | Returns current language code |
useVersion() | Returns current doc version |
usePage() | Returns current page metadata |
usePages() | Returns all pages metadata |
useSite() | Returns site-level configuration |
useFrontmatter() | Returns current page frontmatter |
useI18n() | Returns i18n translation function |