Hide Obsidian app header when scrolling

Edit: Check my replies for more updated/fixed/alternate versions. Feel free to pick whichever one works best for you.

Save this as a .js file and point CodeScript ToolKit to it as your “Startup script path.” It’ll make the Obsidan app header hide when you scroll a note down a little[1] and then show it again when you scroll up.

exports.invoke = async (app) => {
	const duration = 250;
	document.body.style.setProperty('--headertransduration', `${duration}ms`);
	if (window.__scroll_hook_installed || !app.isMobile) return;
	let lastScrollPosition = 0;
	let scrollFlexibility = 1;

	function hookScroll(thing) {
		if (!thing) return;
		const proto = Object.getPrototypeOf(thing);
		if (!proto || typeof proto.trigger !== "function" || proto.trigger.__scroll_hooked) return;

		const orig = proto.trigger;
		proto.trigger = function (eventName, data) {
		try {
			if (eventName === "markdown-scroll") {
				if (data.scroll == 0 || data.scroll <= (lastScrollPosition - scrollFlexibility)) {
					document.body.classList.remove('hide-view-header');
					lastScrollPosition = data.scroll;
				} else if (data.scroll > (lastScrollPosition + scrollFlexibility)) {
					document.body.classList.add('hide-view-header');
					lastScrollPosition = data.scroll;
				}
			}
		} catch (err) {
			console.error(err);
		}
		return orig.apply(this, arguments);
		};

		proto.trigger.__scroll_hooked = true;
	}

	const thingsToHookInto = [app?.workspace, app?.workspace?.rootSplit, app?.workspace?.activeLeaf];

	for (const thing of thingsToHookInto) hookScroll(thing);
	window.__scroll_hook_installed = true;
}

You also need this in your CSS snippets.

body {
	--headertransduration: 0ms;
}

.workspace-leaf-content[data-type="markdown"] .view-header {
	transition: height var(--headertransduration), opacity var(--headertransduration);
}

body.hide-view-header .workspace-leaf-content[data-type="markdown"] .view-header {
	border-bottom: 0;
	height: 0;
    opacity: 0;
	overflow: hidden;
}

Relevant feature requests:


  1. You can change the let scrollFlexibility = 1; to things like let scrollFlexibility = 0.54321; or whatever else you want to dial it in to what feels right. ↩︎

Here’s a version that also hides the Obsidian app header when the softkeyboard is open to make it cover more of what Option to hide/show top bar(s) when note is scrolled & when keyboard appears/disappears - Feature requests - Obsidian Forum is asking for. Don’t forget the CSS in the first post of this thread!

exports.invoke = async (app) => {
	const duration = 250;
	document.body.style.setProperty('--headertransduration', `${duration}ms`);
	const capacitorapp = window.Capacitor?.Plugins?.App;
	let viewHeaderHeight = 0;
	let keyboardopen = false;
	let headerhidden = false;
	let isAutoScrolling = false;

	function adjustScroll(delta) {
	    const editor = app.workspace?.activeLeaf?.view?.editor;
	    if (!editor || app.workspace?.activeLeaf?.view?.file?.extension != 'md') return;

	    const scrollInfo = editor.getScrollInfo();
	    const start = scrollInfo.top;
	    const end = start + delta;
	    const startTime = performance.now();

	    function animate(time) {
	        isAutoScrolling = true;
	        const elapsed = time - startTime;
	        const progress = Math.min(elapsed / duration, 1);
	        const ease = progress < 0.5 
	            ? 2 * progress * progress 
	            : -1 + (4 - 2 * progress) * progress;

	        editor.scrollTo(scrollInfo.left, start + (end - start) * ease);

	        if (progress < 1) {
	            requestAnimationFrame(animate);
	        } else {
				isAutoScrolling = false;
			}
	    }

	    requestAnimationFrame(animate);
	}

	function hideHeader() {
		if (headerhidden || !document.activeElement.classList.contains('cm-content') || document.activeElement.closest('.metadata-property-value') || document.activeElement.closest('.metadata-property-key')) return;
		document.body.classList.add('hide-view-header');
		adjustScroll(-viewHeaderHeight);
		headerhidden = true;
	}

	function showHeader() {
		if (!headerhidden) return;
		document.body.classList.remove('hide-view-header');
		adjustScroll(viewHeaderHeight);
		headerhidden = false;
	}

	function elementDefocused() {
		setTimeout(() => {
			if(!document.activeElement.onblur) document.activeElement.onblur = elementDefocused;
			if (keyboardopen) {
				if (document.activeElement.classList.contains('cm-content')) {
					hideHeader();
				} else if (!document.activeElement.closest('.metadata-property-value') && !document.activeElement.closest('.metadata-property-key')) {
					showHeader();
				}
			}
		}, 100);
	}

	if (capacitorapp?.addListener) {
		window.Capacitor?.Plugins?.Keyboard?.addListener('keyboardWillShow', () => {
			keyboardopen = true;
			hideHeader();
			if(!document.activeElement.onblur) document.activeElement.onblur = elementDefocused;
		});

		window.Capacitor?.Plugins?.Keyboard?.addListener('keyboardWillHide', () => {
			keyboardopen = false;
			showHeader();
		});
	}

	app.workspace.onLayoutReady(() => {
		const leafContent = document.querySelector('.workspace-leaf-content:not([data-type="outgoing-link"]):not([data-type="backlink"]):not([data-type="undefined"])');
		const viewHeader = leafContent?.querySelector('.view-header');
		viewHeaderHeight = viewHeader ? viewHeader.getBoundingClientRect().height : 0;
	});

	if (window.__scroll_hook_installed || !app.isMobile) return;
	let lastScrollPosition = 0;
	let scrollFlexibility = 1;

	function hookScroll(thing) {
		if (!thing) return;
		const proto = Object.getPrototypeOf(thing);
		if (!proto || typeof proto.trigger !== "function" || proto.trigger.__scroll_hooked) return;

		const orig = proto.trigger;
		proto.trigger = function (eventName, data) {
		try {
			if (eventName === "markdown-scroll") {
				if (isAutoScrolling) return;
				if (data.scroll == 0 || data.scroll <= (lastScrollPosition - scrollFlexibility)) {
					document.body.classList.remove('hide-view-header');
					headerhidden = false;
					lastScrollPosition = data.scroll;
				} else if (data.scroll > (lastScrollPosition + scrollFlexibility)) {
					document.body.classList.add('hide-view-header');
					headerhidden = true;
					lastScrollPosition = data.scroll;
				}
			}
		} catch (err) {
			console.error(err);
		}
		return orig.apply(this, arguments);
		};

		proto.trigger.__scroll_hooked = true;
	}

	const thingsToHookInto = [app?.workspace, app?.workspace?.rootSplit, app?.workspace?.activeLeaf];

	for (const thing of thingsToHookInto) hookScroll(thing);
	window.__scroll_hook_installed = true;
}

Updated version of the one that hides the app header in response to scrolling and the softkeyboard hiding/showing

This one now deals with tablets and foldables better. The previous version didn’t hide the tab list when hiding the header.

JS

exports.invoke = async (app) => {
	const duration = 250;
	document.body.style.setProperty('--headertransduration', `${duration}ms`);
	const capacitorapp = window.Capacitor?.Plugins?.App;
	let viewHeaderHeight = parseInt(window.getComputedStyle(document.body).getPropertyValue('--view-header-height'));
	let tabHeaderHeight = parseInt(window.getComputedStyle(document.body).getPropertyValue('--header-height'));
	let keyboardopen = false;
	let headerhidden = false;
	let isAutoScrolling = false;

	function adjustScroll(delta) {
	    const editor = app.workspace?.activeLeaf?.view?.editor;
	    if (!editor || app.workspace?.activeLeaf?.view?.file?.extension != 'md') return;

	    const scrollInfo = editor.getScrollInfo();
	    const start = scrollInfo.top;
	    const end = start + delta;
	    const startTime = performance.now();

	    function animate(time) {
					isAutoScrolling = true;
	        const elapsed = time - startTime;
	        const progress = Math.min(elapsed / duration, 1);
	        const ease = progress < 0.5 
	            ? 2 * progress * progress 
	            : -1 + (4 - 2 * progress) * progress;

	        editor.scrollTo(scrollInfo.left, start + (end - start) * ease);

	        if (progress < 1) {
	            requestAnimationFrame(animate);
	        } else {
				setTimeout(() => {
						isAutoScrolling = false;
					}, duration);
				}
	    }

	    requestAnimationFrame(animate);
	}

	function hideHeader() {
		if (headerhidden || !document.activeElement.classList.contains('cm-content') || document.activeElement.closest('.metadata-property-value') || document.activeElement.closest('.metadata-property-key')) return;
		document.body.classList.add('hide-view-header');
		document.body.classList.contains('is-tablet') ? adjustScroll(-(viewHeaderHeight + tabHeaderHeight)) : adjustScroll(-viewHeaderHeight);
		headerhidden = true;
	}

	function showHeader() {
		if (!headerhidden) return;
		document.body.classList.remove('hide-view-header');
		document.body.classList.contains('is-tablet') ? adjustScroll(viewHeaderHeight + tabHeaderHeight) : adjustScroll(viewHeaderHeight);
		headerhidden = false;
	}

	function elementDefocused() {
		setTimeout(() => {
			if(!document.activeElement.onblur) document.activeElement.onblur = elementDefocused;
			if (keyboardopen) {
				if (document.activeElement.classList.contains('cm-content')) {
					hideHeader();
				} else if (!document.activeElement.closest('.metadata-property-value') && !document.activeElement.closest('.metadata-property-key')) {
					showHeader();
				}
			}
		}, 100);
	}

	if (capacitorapp?.addListener) {
		window.Capacitor?.Plugins?.Keyboard?.addListener('keyboardWillShow', () => {
			keyboardopen = true;
			hideHeader();
			if(!document.activeElement.onblur) document.activeElement.onblur = elementDefocused;
		});

		window.Capacitor?.Plugins?.Keyboard?.addListener('keyboardWillHide', () => {
			keyboardopen = false;
			showHeader();
		});
	}

	let lastScrollPosition = 0;
	let scrollFlexibility = 1;

	function hookScroll(thing) {
		if (window.__scroll_hook_installed || !app.isMobile || !thing) return;
		const proto = Object.getPrototypeOf(thing);
		if (!proto || typeof proto.trigger !== "function" || proto.trigger.__scroll_hooked) return;

		const orig = proto.trigger;
		proto.trigger = function (eventName, data) {
		try {
			if (eventName === "markdown-scroll") {
				if (isAutoScrolling) return;
				if (data.scroll == 0 || data.scroll <= (lastScrollPosition - scrollFlexibility)) {
					document.body.classList.remove('hide-view-header');
					headerhidden = false;
					lastScrollPosition = data.scroll;
				} else if (data.scroll > (lastScrollPosition + scrollFlexibility)) {
					document.body.classList.add('hide-view-header');
					headerhidden = true;
					lastScrollPosition = data.scroll;
				}
			}
		} catch (err) {
			console.error(err);
		}
		return orig.apply(this, arguments);
		};

		proto.trigger.__scroll_hooked = true;
	}

	const thingsToHookInto = [app?.workspace, app?.workspace?.rootSplit, app?.workspace?.activeLeaf];

	for (const thing of thingsToHookInto) hookScroll(thing);
	window.__scroll_hook_installed = true;
}

CSS

body {
	--headertransduration: 0ms;
}

.workspace-leaf-content[data-type="markdown"] .view-header, .workspace-tab-header-container {
	transition: height var(--headertransduration), opacity var(--headertransduration);
}

body.hide-view-header .workspace-leaf-content[data-type="markdown"] .view-header, body.hide-view-header .workspace-tab-header-container {
	border-bottom: 0;
	height: 0;
    opacity: 0;
	overflow: hidden;
}

I’ve combined this with Hide Obsidian app header when the softkeyboard is open - Share & showcase - Obsidian Forum and made a new script. I will no longer be updating this one.