This plugin improves core usability and should be considered for core inclusion.
It adds scroll snapping to markdown headings in Live Preview.
Especially helpful for long notes and mobile scrolling.
Minimal, theme-safe CSS applied directly to CodeMirror.
Makes navigation feel smoother and more intentional without altering content.
A small fix with a big quality-of-life impact.
{
"id": "snap-headings",
"name": "Snap Headings",
"version": "1.0.1",
"minAppVersion": "1.0.0",
"description": "Adds vertical scroll snapping to markdown headings in Live Preview mode for smooth navigation.",
"author": "Custom Plugin",
"authorUrl": "",
"main": "main.js"
}
import { Plugin } from "obsidian";
export default class SnapHeadingsPlugin extends Plugin {
private styleElement: HTMLStyleElement | null = null;
onload() {
this.applySnapStyles();
// Re-apply styles when switching between modes
this.registerEvent(
this.app.workspace.on('layout-change', () => {
setTimeout(() => this.applySnapStyles(), 100);
})
);
}
applySnapStyles() {
// Remove existing styles first
this.removeStyles();
const style = document.createElement("style");
style.id = "snap-headings-style";
// Mobile-friendly CSS without :has() selector
style.textContent = `
/* Enable vertical snap scrolling for editor */
.cm-scroller {
scroll-snap-type: y mandatory;
scroll-padding-top: 2rem;
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
}
/* Snap to heading lines - more specific selectors for better mobile support */
.cm-line .cm-header-1,
.cm-line .cm-header-2,
.cm-line .cm-header-3,
.cm-line .cm-header-4,
.cm-line .cm-header-5,
.cm-line .cm-header-6 {
scroll-snap-align: start;
}
/* Fallback: snap to lines containing headers */
.cm-line:has(.cm-header-1),
.cm-line:has(.cm-header-2),
.cm-line:has(.cm-header-3),
.cm-line:has(.cm-header-4),
.cm-line:has(.cm-header-5),
.cm-line:has(.cm-header-6) {
scroll-snap-align: start;
}
/* Mobile-specific adjustments */
@media (max-width: 768px) {
.cm-scroller {
scroll-padding-top: 1rem;
}
}
/* Also apply to reading view */
.markdown-preview-view .markdown-preview-sizer {
scroll-snap-type: y mandatory;
scroll-padding-top: 2rem;
}
.markdown-preview-view h1,
.markdown-preview-view h2,
.markdown-preview-view h3,
.markdown-preview-view h4,
.markdown-preview-view h5,
.markdown-preview-view h6 {
scroll-snap-align: start;
}
`;
document.head.appendChild(style);
this.styleElement = style;
}
removeStyles() {
if (this.styleElement) {
this.styleElement.remove();
this.styleElement = null;
}
// Also remove any existing styles with the old ID
const existing = document.getElementById("snap-headings-style");
if (existing) existing.remove();
}
onunload() {
this.removeStyles();
}
}
const { Plugin } = require("obsidian");
class SnapHeadingsPlugin extends Plugin {
constructor() {
super(...arguments);
this.styleElement = null;
}
onload() {
this.applySnapStyles();
// Re-apply styles when switching between modes
this.registerEvent(
this.app.workspace.on('layout-change', () => {
setTimeout(() => this.applySnapStyles(), 100);
})
);
}
applySnapStyles() {
// Remove existing styles first
this.removeStyles();
const style = document.createElement("style");
style.id = "snap-headings-style";
// Mobile-friendly CSS without :has() selector
style.textContent = `
/* Enable vertical snap scrolling for editor */
.cm-scroller {
scroll-snap-type: y proximity;
scroll-padding-top: 2rem;
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
}
/* More aggressive snapping on desktop */
@media (hover: hover) and (pointer: fine) {
.cm-scroller {
scroll-snap-type: y mandatory;
}
}
/* Snap to heading lines - more specific selectors for better mobile support */
.cm-line .cm-header-1,
.cm-line .cm-header-2,
.cm-line .cm-header-3,
.cm-line .cm-header-4,
.cm-line .cm-header-5,
.cm-line .cm-header-6 {
scroll-snap-align: start;
}
/* Fallback: snap to lines containing headers */
.cm-line:has(.cm-header-1),
.cm-line:has(.cm-header-2),
.cm-line:has(.cm-header-3),
.cm-line:has(.cm-header-4),
.cm-line:has(.cm-header-5),
.cm-line:has(.cm-header-6) {
scroll-snap-align: start;
}
/* Mobile-specific adjustments */
@media (max-width: 768px) {
.cm-scroller {
scroll-padding-top: 1rem;
}
}
/* Also apply to reading view - less aggressive on mobile */
.markdown-preview-view .markdown-preview-sizer {
scroll-snap-type: y proximity;
scroll-padding-top: 2rem;
}
/* More aggressive snapping on desktop for reading view */
@media (hover: hover) and (pointer: fine) {
.markdown-preview-view .markdown-preview-sizer {
scroll-snap-type: y mandatory;
}
}
.markdown-preview-view h1,
.markdown-preview-view h2,
.markdown-preview-view h3,
.markdown-preview-view h4,
.markdown-preview-view h5,
.markdown-preview-view h6 {
scroll-snap-align: start;
}
`;
document.head.appendChild(style);
this.styleElement = style;
}
removeStyles() {
if (this.styleElement) {
this.styleElement.remove();
this.styleElement = null;
}
// Also remove any existing styles with the old ID
const existing = document.getElementById("snap-headings-style");
if (existing) existing.remove();
}
onunload() {
this.removeStyles();
}
}
module.exports = SnapHeadingsPlugin;
This plugin is a third-party community contribution and is not affiliated with Obsidian.md
This pseudonym is not affiliated with Obsidian.md
© 2025 obsidiannow. All rights reserved.