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:

  1. CSS Variables: Modify colors, fonts, and spacing
  2. Theme Configuration: Set options in config file
  3. Custom Components: Override default components
  4. 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

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
  }
});

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
  }
});

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 hook

Theme 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);
}

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

  1. 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 */
}
  1. 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

  1. Styles Not Applied
// Ensure CSS is imported in config
export default defineConfig({
  builderConfig: {
    html: {
      tags: [
        {
          tag: 'link',
          attrs: {
            rel: 'stylesheet',
            href: '/styles/custom.css'
          }
        }
      ]
    }
  }
});
  1. Dark Mode Not Working
/* Ensure correct class name */
.dark {
  /* Dark mode styles */
}

/* Or use data attribute */
[data-theme="dark"] {
  /* Dark mode styles */
}
  1. Component Not Overriding
// Ensure component is exported
export { CustomNavbar as Navbar } from './components/CustomNavbar';

Next Steps


::: tip 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 :::

::: warning Performance

  • Avoid excessive CSS selectors
  • Optimize images and assets
  • Use code splitting for large components
  • Minimize repaints and reflows :::

::: info Resources