Skip to content

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:

css
/* 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:

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

css
/* 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:

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

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

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

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

tsx
// 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>
  );
}
css
/* 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:

tsx
// 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>
  );
}
css
/* 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

typescript
// 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

tsx
// 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

typescript
// 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:

css
/* 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:

css
/* 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:

css
/* 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:

css
/* 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:

css
/* 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
css
/* 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
typescript
// theme/index.ts
import { lazy } from 'react';

const HeavyComponent = lazy(() => import('./components/HeavyComponent'));

Accessibility

Ensure theme accessibility:

css
/* 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:

typescript
// 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
typescript
// 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
css
/* Ensure correct class name */
.dark {
  /* Dark mode styles */
}

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

Next Steps


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

Content is for learning and research only.