Jacky Zhao
2025-04-18 b34d521293415944370fd0f5cf25cd71bcffb5b6
feat: reader mode
4 files added
4 files modified
140 ■■■■■ changed files
docs/features/reader mode.md 44 ●●●●● patch | view | raw | blame | history
index.d.ts 1 ●●●● patch | view | raw | blame | history
quartz.layout.ts 1 ●●●● patch | view | raw | blame | history
quartz/components/ReaderMode.tsx 32 ●●●●● patch | view | raw | blame | history
quartz/components/index.ts 2 ●●●●● patch | view | raw | blame | history
quartz/components/scripts/readermode.inline.ts 25 ●●●●● patch | view | raw | blame | history
quartz/components/styles/darkmode.scss 2 ●●● patch | view | raw | blame | history
quartz/components/styles/readermode.scss 33 ●●●●● patch | view | raw | blame | history
docs/features/reader mode.md
New file
@@ -0,0 +1,44 @@
---
title: Reader Mode
tags:
  - component
---
Reader Mode is a feature that allows users to focus on the content by hiding the sidebars and other UI elements. When enabled, it provides a clean, distraction-free reading experience.
## Configuration
Reader Mode is enabled by default. To disable it, you can remove the component from your layout configuration in `quartz.layout.ts`:
```ts
// Remove or comment out this line
Component.ReaderMode(),
```
## Usage
The Reader Mode toggle appears as a button with a book icon. When clicked:
- Sidebars are hidden
- Hovering over the content area reveals the sidebars temporarily
Unlike Dark Mode, Reader Mode state is not persisted between page reloads but is maintained during SPA navigation within the site.
## Customization
You can customize the appearance of Reader Mode through CSS variables and styles. The component uses the following classes:
- `.readermode`: The toggle button
- `.readerIcon`: The book icon
- `[reader-mode="on"]`: Applied to the root element when Reader Mode is active
Example customization in your custom CSS:
```scss
.readermode {
  // Customize the button
  svg {
    stroke: var(--custom-color);
  }
}
```
index.d.ts
@@ -8,6 +8,7 @@
  prenav: CustomEvent<{}>
  nav: CustomEvent<{ url: FullSlug }>
  themechange: CustomEvent<{ theme: "light" | "dark" }>
  readermodechange: CustomEvent<{ mode: "on" | "off" }>
}
type ContentIndex = Record<FullSlug, ContentDetails>
quartz.layout.ts
@@ -35,6 +35,7 @@
          grow: true,
        },
        { Component: Component.Darkmode() },
        { Component: Component.ReaderMode() },
      ],
    }),
    Component.Explorer(),
quartz/components/ReaderMode.tsx
New file
@@ -0,0 +1,32 @@
// @ts-ignore
import readerModeScript from "./scripts/readermode.inline"
import styles from "./styles/readermode.scss"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
const ReaderMode: QuartzComponent = ({ displayClass }: QuartzComponentProps) => {
  return (
    <button class={classNames(displayClass, "readermode")}>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        class="readerIcon"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <rect x="6" y="4" width="12" height="16" rx="1"></rect>
        <line x1="9" y1="8" x2="15" y2="8"></line>
        <line x1="9" y1="12" x2="15" y2="12"></line>
        <line x1="9" y1="16" x2="13" y2="16"></line>
      </svg>
    </button>
  )
}
ReaderMode.beforeDOMLoaded = readerModeScript
ReaderMode.css = styles
export default (() => ReaderMode) satisfies QuartzComponentConstructor
quartz/components/index.ts
@@ -4,6 +4,7 @@
import NotFound from "./pages/404"
import ArticleTitle from "./ArticleTitle"
import Darkmode from "./Darkmode"
import ReaderMode from "./ReaderMode"
import Head from "./Head"
import PageTitle from "./PageTitle"
import ContentMeta from "./ContentMeta"
@@ -29,6 +30,7 @@
  TagContent,
  FolderContent,
  Darkmode,
  ReaderMode,
  Head,
  PageTitle,
  ContentMeta,
quartz/components/scripts/readermode.inline.ts
New file
@@ -0,0 +1,25 @@
let isReaderMode = false
const emitReaderModeChangeEvent = (mode: "on" | "off") => {
  const event: CustomEventMap["readermodechange"] = new CustomEvent("readermodechange", {
    detail: { mode },
  })
  document.dispatchEvent(event)
}
document.addEventListener("nav", () => {
  const switchReaderMode = () => {
    isReaderMode = !isReaderMode
    const newMode = isReaderMode ? "on" : "off"
    document.documentElement.setAttribute("reader-mode", newMode)
    emitReaderModeChangeEvent(newMode)
  }
  for (const readerModeButton of document.getElementsByClassName("readermode")) {
    readerModeButton.addEventListener("click", switchReaderMode)
    window.addCleanup(() => readerModeButton.removeEventListener("click", switchReaderMode))
  }
  // Set initial state
  document.documentElement.setAttribute("reader-mode", isReaderMode ? "on" : "off")
})
quartz/components/styles/darkmode.scss
@@ -6,7 +6,7 @@
  border: none;
  width: 20px;
  height: 20px;
  margin: 0 10px;
  margin: 0;
  text-align: inherit;
  flex-shrink: 0;
quartz/components/styles/readermode.scss
New file
@@ -0,0 +1,33 @@
.readermode {
  cursor: pointer;
  padding: 0;
  position: relative;
  background: none;
  border: none;
  width: 20px;
  height: 20px;
  margin: 0;
  text-align: inherit;
  flex-shrink: 0;
  & svg {
    position: absolute;
    width: 20px;
    height: 20px;
    top: calc(50% - 10px);
    stroke: var(--darkgray);
    transition: opacity 0.1s ease;
  }
}
:root[reader-mode="on"] {
  & .sidebar.left,
  & .sidebar.right {
    opacity: 0;
    transition: opacity 0.2s ease;
    &:hover {
      opacity: 1;
    }
  }
}