Custom Theme
Rspress provides powerful theme customization capabilities, allowing you to create documentation sites that match your brand identity. This chapter explores how to customize theme styles, create custom themes, and adjust layouts.
Theme System Overview
Default Theme
Rspress comes with a fully-featured default theme:
- Responsive layout
- Dark/light mode
- Customizable color schemes
- Flexible component system
- Accessibility features
Customization Options
Themes can be customized through:
- CSS Variables: Modify colors, fonts, and spacing
- Theme Configuration: Set options in config file
- Custom Components: Override default components
- Full Custom Theme: Create a new theme from scratch
Using CSS Variables
Basic Customization
Override CSS variables in a custom CSS file:
/* styles/custom.css */
:root {
/* Brand colors */
--rp-c-brand: #667eea;
--rp-c-brand-dark: #5568d3;
--rp-c-brand-light: #8b9dff;
/* Background colors */
--rp-c-bg: #ffffff;
--rp-c-bg-soft: #f9f9f9;
--rp-c-bg-mute: #f1f1f1;
/* Text colors */
--rp-c-text-1: #213547;
--rp-c-text-2: #476582;
--rp-c-text-3: #7d8590;
/* Border colors */
--rp-c-divider: #e2e8f0;
--rp-c-border: #c9d3df;
}Import in rspress.config.ts:
import { defineConfig } from 'rspress/config';
export default defineConfig({
// ... other config
builderConfig: {
html: {
tags: [
{
tag: 'link',
attrs: {
rel: 'stylesheet',
href: '/styles/custom.css'
}
}
]
}
}
});Dark Mode Customization
Customize dark mode colors:
/* Dark mode variables */
.dark {
/* Brand colors (dark mode) */
--rp-c-brand: #8b9dff;
--rp-c-brand-dark: #667eea;
--rp-c-brand-light: #a0aeff;
/* Background colors (dark mode) */
--rp-c-bg: #1a1a1a;
--rp-c-bg-soft: #252525;
--rp-c-bg-mute: #2f2f2f;
/* Text colors (dark mode) */
--rp-c-text-1: #f0f0f0;
--rp-c-text-2: #c9d1d9;
--rp-c-text-3: #8b949e;
/* Border colors (dark mode) */
--rp-c-divider: #30363d;
--rp-c-border: #3d444d;
}Typography Customization
Customize fonts and spacing:
:root {
/* Fonts */
--rp-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--rp-font-family-mono: 'Fira Code', 'Courier New', monospace;
/* Font sizes */
--rp-font-size-root: 16px;
--rp-font-size-xs: 12px;
--rp-font-size-sm: 14px;
--rp-font-size-md: 16px;
--rp-font-size-lg: 18px;
--rp-font-size-xl: 20px;
/* Line heights */
--rp-line-height-tight: 1.25;
--rp-line-height-base: 1.5;
--rp-line-height-relaxed: 1.75;
/* Spacing */
--rp-space-1: 4px;
--rp-space-2: 8px;
--rp-space-3: 12px;
--rp-space-4: 16px;
--rp-space-5: 20px;
--rp-space-6: 24px;
--rp-space-8: 32px;
}Theme Configuration
Navbar Configuration
Customize navbar appearance:
import { defineConfig } from 'rspress/config';
export default defineConfig({
title: 'My Documentation',
themeConfig: {
// Navbar configuration
nav: [
{ text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/' },
{ text: 'About', link: '/about' }
],
// Logo
logo: '/logo.png',
logoText: 'My Project',
// Social links
socialLinks: [
{
icon: 'github',
link: 'https://github.com/username/project'
},
{
icon: 'twitter',
link: 'https://twitter.com/username'
},
{
icon: 'discord',
link: 'https://discord.gg/invite'
}
],
// Navbar appearance
appearance: true, // Enable dark mode toggle
}
});Sidebar Configuration
Customize sidebar styling:
export default defineConfig({
themeConfig: {
sidebar: {
'/guide/': [
{
text: 'Getting Started',
items: [
{ text: 'Introduction', link: '/guide/introduction' },
{ text: 'Quick Start', link: '/guide/quick-start' }
]
}
]
},
// Sidebar settings
outlineTitle: 'On This Page',
outline: [2, 3], // Show h2 and h3 headings
}
});Footer Configuration
Add custom footer:
export default defineConfig({
themeConfig: {
footer: {
message: 'Released under the MIT License',
copyright: 'Copyright © 2024 My Project'
},
// Previous/Next page links
docFooter: {
prev: 'Previous',
next: 'Next'
},
// Edit link
editLink: {
pattern: 'https://github.com/username/project/edit/main/docs/:path',
text: 'Edit this page on GitHub'
},
// Last updated time
lastUpdatedText: 'Last Updated'
}
});Component Customization
Overriding Default Components
Create custom components to override defaults:
// theme/components/CustomNavbar.tsx
import React from 'react';
export function CustomNavbar() {
return (
<nav className="custom-navbar">
<div className="navbar-brand">
<img src="/logo.png" alt="Logo" />
<span>My Project</span>
</div>
<div className="navbar-links">
<a href="/guide">Guide</a>
<a href="/api">API</a>
<a href="/blog">Blog</a>
</div>
<div className="navbar-actions">
<button className="search-button">Search</button>
<button className="theme-toggle">🌙</button>
</div>
</nav>
);
}/* theme/components/CustomNavbar.css */
.custom-navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: var(--rp-c-bg);
border-bottom: 1px solid var(--rp-c-divider);
}
.navbar-brand {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: bold;
}
.navbar-brand img {
width: 32px;
height: 32px;
}
.navbar-links {
display: flex;
gap: 2rem;
}
.navbar-links a {
color: var(--rp-c-text-1);
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
}
.navbar-links a:hover {
color: var(--rp-c-brand);
}
.navbar-actions {
display: flex;
gap: 1rem;
}
.search-button,
.theme-toggle {
padding: 0.5rem 1rem;
border: 1px solid var(--rp-c-border);
background: var(--rp-c-bg);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.search-button:hover,
.theme-toggle:hover {
background: var(--rp-c-bg-soft);
border-color: var(--rp-c-brand);
}Custom Home Page
Create a custom home page component:
// theme/components/CustomHome.tsx
import React from 'react';
import './CustomHome.css';
export function CustomHome() {
return (
<div className="custom-home">
{/* Hero section */}
<section className="hero">
<h1 className="hero-title">
<span className="gradient-text">Next Generation</span>
<br />
Documentation Framework
</h1>
<p className="hero-subtitle">
Fast, modern, and powerful documentation solution
</p>
<div className="hero-actions">
<a href="/guide/quick-start" className="btn btn-primary">
Get Started
</a>
<a href="/guide/introduction" className="btn btn-secondary">
Learn More
</a>
</div>
</section>
{/* Features section */}
<section className="features">
<div className="container">
<div className="feature-grid">
<div className="feature-card">
<div className="feature-icon">⚡</div>
<h3>Lightning Fast</h3>
<p>Built with Vite for blazing fast dev and build times</p>
</div>
<div className="feature-card">
<div className="feature-icon">🎨</div>
<h3>Fully Customizable</h3>
<p>Complete control over themes and styling</p>
</div>
<div className="feature-card">
<div className="feature-icon">📱</div>
<h3>Responsive</h3>
<p>Works perfectly on all devices</p>
</div>
<div className="feature-card">
<div className="feature-icon">🔍</div>
<h3>Powerful Search</h3>
<p>Built-in full-text search capabilities</p>
</div>
<div className="feature-card">
<div className="feature-icon">🌍</div>
<h3>i18n Ready</h3>
<p>Built-in support for multiple languages</p>
</div>
<div className="feature-card">
<div className="feature-icon">⚙️</div>
<h3>Extensible</h3>
<p>Extend with plugins and themes</p>
</div>
</div>
</div>
</section>
</div>
);
}/* theme/components/CustomHome.css */
.custom-home {
width: 100%;
}
.hero {
text-align: center;
padding: 120px 20px;
background: linear-gradient(135deg,
rgba(102, 126, 234, 0.1) 0%,
rgba(118, 75, 162, 0.1) 100%);
}
.hero-title {
font-size: 4rem;
font-weight: 800;
margin-bottom: 1rem;
line-height: 1.2;
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: 1.5rem;
color: var(--rp-c-text-2);
margin-bottom: 2rem;
}
.hero-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
padding: 12px 32px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s;
display: inline-block;
}
.btn-primary {
background: var(--rp-c-brand);
color: white;
border: 2px solid var(--rp-c-brand);
}
.btn-primary:hover {
background: var(--rp-c-brand-dark);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: transparent;
color: var(--rp-c-brand);
border: 2px solid var(--rp-c-brand);
}
.btn-secondary:hover {
background: var(--rp-c-brand);
color: white;
}
.features {
padding: 80px 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
}
.feature-card {
padding: 30px;
border: 1px solid var(--rp-c-divider);
border-radius: 12px;
transition: all 0.3s;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border-color: var(--rp-c-brand);
}
.feature-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: var(--rp-c-text-1);
}
.feature-card p {
color: var(--rp-c-text-2);
line-height: 1.6;
}
@media (max-width: 768px) {
.hero-title {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.25rem;
}
.hero-actions {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
max-width: 300px;
}
.feature-grid {
grid-template-columns: 1fr;
}
}Creating a Custom Theme
Theme Structure
Create a complete custom theme:
theme/
├── index.ts # Theme entry
├── components/
│ ├── Layout.tsx # Main layout
│ ├── Navbar.tsx # Navigation bar
│ ├── Sidebar.tsx # Sidebar
│ ├── Content.tsx # Content area
│ └── Footer.tsx # Footer
├── styles/
│ ├── vars.css # CSS variables
│ ├── base.css # Base styles
│ └── components.css # Component styles
└── hooks/
├── useTheme.ts # Theme hook
└── useNav.ts # Navigation hookTheme Entry
// theme/index.ts
import { Theme } from 'rspress/theme';
import { Layout } from './components/Layout';
import './styles/vars.css';
import './styles/base.css';
import './styles/components.css';
const theme: Theme = {
Layout,
// Register other components...
};
export default theme;
export * from './components';
export * from './hooks';Custom Layout Component
// theme/components/Layout.tsx
import React from 'react';
import { usePageData } from 'rspress/runtime';
import { Navbar } from './Navbar';
import { Sidebar } from './Sidebar';
import { Content } from './Content';
import { Footer } from './Footer';
export function Layout() {
const pageData = usePageData();
const { frontmatter } = pageData;
// Check if it's a custom page
if (frontmatter.pageType === 'custom') {
return <>{pageData.content}</>;
}
return (
<div className="theme-layout">
<Navbar />
<div className="layout-container">
{frontmatter.sidebar !== false && <Sidebar />}
<Content />
</div>
<Footer />
</div>
);
}Theme Hook
// theme/hooks/useTheme.ts
import { useState, useEffect } from 'react';
export function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
// Read theme from localStorage
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark';
if (savedTheme) {
setTheme(savedTheme);
document.documentElement.classList.toggle('dark', savedTheme === 'dark');
}
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
};
return { theme, toggleTheme };
}Layout Customization
Responsive Layout
Create a responsive layout system:
/* theme/styles/layout.css */
.theme-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.layout-container {
display: flex;
flex: 1;
max-width: 1400px;
margin: 0 auto;
width: 100%;
padding: 0 24px;
}
/* Sidebar */
.sidebar {
width: 280px;
flex-shrink: 0;
padding: 24px 0;
position: sticky;
top: 64px;
height: calc(100vh - 64px);
overflow-y: auto;
}
/* Content */
.content {
flex: 1;
min-width: 0;
padding: 24px 48px;
}
/* Table of contents */
.outline {
width: 240px;
flex-shrink: 0;
padding: 24px 0;
position: sticky;
top: 64px;
height: calc(100vh - 64px);
overflow-y: auto;
}
/* Responsive */
@media (max-width: 1200px) {
.outline {
display: none;
}
}
@media (max-width: 960px) {
.sidebar {
position: fixed;
left: 0;
top: 64px;
transform: translateX(-100%);
transition: transform 0.3s;
background: var(--rp-c-bg);
z-index: 100;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
.sidebar.open {
transform: translateX(0);
}
.layout-container {
padding: 0 16px;
}
.content {
padding: 24px 0;
}
}Grid System
Implement a flexible grid system:
/* theme/styles/grid.css */
.grid {
display: grid;
gap: var(--grid-gap, 24px);
}
.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
/* Auto fill */
.grid-auto-fit {
grid-template-columns: repeat(auto-fit, minmax(var(--grid-min, 300px), 1fr));
}
.grid-auto-fill {
grid-template-columns: repeat(auto-fill, minmax(var(--grid-min, 300px), 1fr));
}
/* Responsive */
@media (max-width: 768px) {
.grid-cols-2,
.grid-cols-3,
.grid-cols-4 {
grid-template-columns: 1fr;
}
}Advanced Theme Features
Animations and Transitions
Add smooth animations:
/* theme/styles/animations.css */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
transform: translateX(-20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
.slide-in {
animation: slideIn 0.3s ease-out;
}
/* Page transitions */
.page-transition-enter {
opacity: 0;
transform: translateY(20px);
}
.page-transition-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
}
.page-transition-exit {
opacity: 1;
}
.page-transition-exit-active {
opacity: 0;
transition: opacity 0.2s;
}Custom Scrollbar
Style the scrollbar:
/* theme/styles/scrollbar.css */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--rp-c-bg);
}
::-webkit-scrollbar-thumb {
background: var(--rp-c-border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--rp-c-brand);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--rp-c-border) var(--rp-c-bg);
}Print Styles
Optimize for printing:
/* theme/styles/print.css */
@media print {
/* Hide navigation elements */
.navbar,
.sidebar,
.outline,
.footer,
.edit-link {
display: none !important;
}
/* Full width content */
.content {
max-width: 100%;
padding: 0;
}
/* Optimize typography */
body {
font-size: 12pt;
line-height: 1.5;
color: #000;
background: #fff;
}
/* Keep links visible */
a[href]:after {
content: " (" attr(href) ")";
font-size: 10pt;
color: #666;
}
/* Page break control */
h1, h2, h3 {
page-break-after: avoid;
}
pre, blockquote {
page-break-inside: avoid;
}
}Best Practices
Performance Optimization
- CSS Optimization
/* Use CSS custom properties */
:root {
--primary-color: #667eea;
}
/* Avoid complex selectors */
.simple-class { } /* Good */
div > ul > li > a { } /* Avoid */
/* Use transform instead of position */
.animated {
transform: translateX(10px); /* Good */
/* left: 10px; */ /* Avoid */
}- Code Splitting
// theme/index.ts
import { lazy } from 'react';
const HeavyComponent = lazy(() => import('./components/HeavyComponent'));Accessibility
Ensure theme accessibility:
/* Focus styles */
:focus-visible {
outline: 2px solid var(--rp-c-brand);
outline-offset: 2px;
}
/* Skip link */
.skip-to-content {
position: absolute;
top: -40px;
left: 0;
background: var(--rp-c-brand);
color: white;
padding: 8px 16px;
text-decoration: none;
z-index: 100;
}
.skip-to-content:focus {
top: 0;
}Theme Testing
Test theme components:
// theme/__tests__/Layout.test.tsx
import { render, screen } from '@testing-library/react';
import { Layout } from '../components/Layout';
describe('Layout', () => {
it('renders navbar', () => {
render(<Layout />);
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
it('renders content area', () => {
render(<Layout />);
expect(screen.getByRole('main')).toBeInTheDocument();
});
});Troubleshooting
Common Issues
- Styles Not Applied
// Ensure CSS is imported in config
export default defineConfig({
builderConfig: {
html: {
tags: [
{
tag: 'link',
attrs: {
rel: 'stylesheet',
href: '/styles/custom.css'
}
}
]
}
}
});- Dark Mode Not Working
/* Ensure correct class name */
.dark {
/* Dark mode styles */
}
/* Or use data attribute */
[data-theme="dark"] {
/* Dark mode styles */
}- Component Not Overriding
// Ensure component is exported
export { CustomNavbar as Navbar } from './components/CustomNavbar';Next Steps
- Learn about Versioning
- Explore Built-in Components
- Read about Build Extensions
Best Practices
- Use CSS variables for maintainability
- Ensure both dark and light modes are tested
- Optimize for mobile devices
- Follow accessibility guidelines
- Document custom theme components
Performance
- Avoid excessive CSS selectors
- Optimize images and assets
- Use code splitting for large components
- Minimize repaints and reflows