Hide Obsidian app header when scrolling and opening your softkeyboard

This merges Hide Obsidian app header when the softkeyboard is open - Share & showcase - Obsidian Forum and Hide Obsidian app header when scrolling - Share & showcase - Obsidian Forum into one script. It also makes some pretty major changes with how it works.

Save this as a .js file and point CodeScript ToolKit to it as your “Startup script path.” Also, add the CSS to your Obsidian CSS snippets. It’ll make the Obsidan app header hide when the softkeyboard is open and show when it’s not. It’ll also make the Obsidan app header hide when you scroll a note down a little[1] and then show it again when you scroll up.

How it works: it makes the Obsidian app header float over the editor/note content instead of being stacked above it[2][3]. Then, to avoid the header covering your note content, when you’re fully scrolled up, it adds some padding to the top of the note content[4]. It also has animations for when the header shows and hides[5].

JS

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

	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');
		headerhidden = true;
	}

	function showHeader() {
		if (!headerhidden) return;
		document.body.classList.remove('hide-view-header');
		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 ((data.scroll ?? 0) === 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;
				}
				document.body.classList.toggle('is-scrolled', data.scroll !== 0);
			}
		} 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;

	app.workspace.on('active-leaf-change', () => {
		if ((app.workspace.activeLeaf?.view?.scroll ?? 0) === 0 || app.workspace.activeLeaf?.view?.file?.extension != "md") {
			document.body.classList.remove('is-scrolled');
			document.body.classList.remove('hide-view-header');
		} else {
			document.body.classList.add('is-scrolled');
		}
	});
}

CSS

body {
	--headertransduration: 0ms;
}

.is-mobile .mod-root .view-header, .is-mobile .workspace-tab-header-container {
	transition: transform var(--headertransduration), opacity var(--headertransduration);
	position: absolute;
	z-index: 2;
	left: 0;
	right: 0;
}

.is-mobile .mod-root .view-content {
	transition: padding-top var(--headertransduration);
	padding-top: var(--view-header-height);
}

.is-tablet .mod-root .view-content {
	padding-top: calc(var(--view-header-height) + var(--header-height));
}

.is-tablet .mod-root .view-header {
	top: var(--header-height);
}

body.is-scrolled .mod-root .view-content {
	padding-top: 0;
}

body.hide-view-header .mod-root .view-header, body.hide-view-header .workspace-tab-header-container {
	transform: translateY(-100%);
	opacity: 0;
	pointer-events: none;
}

Optionally, if you want the mobile navbar to also hide whenever the app header hides, add this CSS:

.mobile-navbar {
	transition: transform var(--headertransduration), opacity var(--headertransduration);
	position: fixed;
	z-index: 2;
	left: 0;
	right: 0;
	bottom: 0;
}

body.hide-view-header .mobile-navbar {
	transform: translateY(100%);
	opacity: 0;
	pointer-events: none;
}

  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. ↩︎

  2. And, since the tablet tab bar and the app header are separate elements, it deals with that too. ↩︎

  3. Also, this makes it so things don’t jump as much, so there’s no need to compensate for it with automatically scrolling like some of my older scripts were doing. ↩︎

  4. Different padding depending on if you’re in tablet mode or not. ↩︎

  5. Change const duration = 250; to whatever you want to change the animation speed. It’s in milliseconds. ↩︎

2 Likes

Thanks again for this excellent, complete workaround to Option to hide/show top bar(s) when note is scrolled & when keyboard appears/disappears!

1 Like

Noticed some issues with searching a file and graph view.

  1. The search would pop up over the app header if you weren’t at the top of the note and the header was not hidden
  2. If you use Advanced Canvas’ search, it would be somewhat hidden behind the app header
  3. Graph view’s options that are shown at the top right would be slightly hidden behind the app header

Here’s updated CSS that fixes those things. If you added the CSS that hides the mobile navigation toolbar, don’t forget to re-add it below this stuff.

body {
	--headertransduration: 0ms;
}

body.is-mobile .mod-root .view-header, body.is-mobile .workspace-tab-header-container {
	transition: transform var(--headertransduration), opacity var(--headertransduration);
	position: absolute;
	z-index: 2;
	left: 0;
	right: 0;
}

body.is-mobile .mod-root .view-content {
	transition: padding-top var(--headertransduration);
	--content-top-padding: var(--view-header-height);
	padding-top: var(--content-top-padding);
}

body.is-tablet .mod-root .view-content {
	--content-top-padding: calc(var(--view-header-height) + var(--header-height));
	padding-top: var(--content-top-padding);
}

body.is-mobile .document-search-container {
	transition: transform var(--headertransduration);
}

body:not(.hide-view-header).is-scrolled.is-mobile .document-search-container, body.is-mobile .workspace-leaf-content[data-type="canvas"] .document-search-container, .graph-controls {
	transform: translateY(var(--content-top-padding));
}

body.is-scrolled .mod-root .view-content {
	padding-top: 0;
}

body.hide-view-header .mod-root .view-header, body.hide-view-header .workspace-tab-header-container {
	transform: translateY(-100%);
	opacity: 0;
	pointer-events: none;
}

Also, on some (or all?) iOS devices, this line that should animate adding the padding when you scroll all the way to the top of a note and animate removing the padding when you scroll down causes jumpiness, so you can remove it if you want:
transition: padding-top var(--headertransduration);

Removing this can cause a different, lesser jump as the padding appears or disappears all at once. It’s not noticeable when scrolling quickly, but if the scroll loses momentum as it reaches the top of the note, the last bit of text will pop out from behind the header (or if you idly nudge the page upward when at the top, the text will pop upward). I definitely prefer it to the back-and forth jumpiness that sometimes happens with the animation, tho.

Thanks to both of you, @Abisheik and @CawlinTeffid .

Have you made any tweaks since then?

Nah, I stopped using it when Obsidian 1.11.0 came out since the official/native implementation of the feature works pretty well. I know it doesn’t work on tablets, though, so I guess my script still has some use.

I’m still using it because the official implementation doesn’t work when editing, which is when I want it most and the main point of the feature request. :sob: But I haven’t made any tweaks because I’m just a suggester & user.

Ah, right… I looked into it and I think I know why. The official version makes the system status bar also hide along with the UI stuff[1]. Toggling the system status bar while the softkeyboard is open triggers the listener that listens for the keyboard to think the softkeyboard was closed and opened quickly which then can cause an infinite loop. Like…

  1. Keyboard open, system status bar and UI stuff hide
  2. Scroll up, system status bar and UI stuff show
  3. Even though the keyboard is still open, Obsidian thinks it was closed and re-opened
  4. Keybord close means show status bar and UI stuff, but it’s already showing so that’s fine
  5. Keybord open means hide status bar and UI stuff, so the status bar’s visibility changes
  6. Since the status bar’s visibility changed, Obsidian thinks the softkeyboard was closed and re-opened
  7. Keyboard close means the status bar visibility should change again to visible
  8. Since the status bar’s visibility changed, Obsidian thinks the softkeyboard was closed and re-opened
  9. Keyboard open means the status bar visibility should change again to hidden

Loop steps 6-to-9 over and over again forever.

So, I did something. I made two different versions.

This no longer needs any extra CSS, but, since it uses the hiding/showing built into Obsidian, it will not work on tablets.


Version that doesn’t toggle the status bar if the softkeyboard is open when hiding and showing the UI stuff

So, that means the status bar’s visibility will stay whatever it was before you opened the softkeyboard.

exports.invoke = async (app) => {
	const capacitorapp = window.Capacitor?.Plugins?.App;
	let keyboardOpen = false;
	let uiStuffShown = "yes";
	let realHideHeaderNav = function() {};
	let realShowHeaderNav = function() {};

	function hideUI() {
		if (document.activeElement.closest('.metadata-property-value') || document.activeElement.closest('.metadata-property-key')) return;
		if (!keyboardOpen && uiStuffShown != "no") {
			realHideHeaderNav();
			uiStuffShown = "no";
		} else if (keyboardOpen && uiStuffShown != "sorta") {
			document.body.classList.add('is-hidden-nav');
			uiStuffShown = "sorta";
		}
	}

	function showUI() {
		if (!keyboardOpen && uiStuffShown != "yes") {
			realHideHeaderNav();
			realShowHeaderNav();
			uiStuffShown = "yes";
		} else if (keyboardOpen && uiStuffShown != "kinda") {
			document.body.classList.remove('is-hidden-nav');
			uiStuffShown = "kinda";
		}
	}

	document.addEventListener('focusin', () => {
		if (!keyboardOpen) return;
		setTimeout(() => {
			const active = document.activeElement;
			if (!active) return;

			if (active.classList.contains('cm-content')) {
				hideUI();
			} else if (!active.closest('.metadata-property-value') && !active.closest('.metadata-property-key')) {
				showUI();
			}
		}, 100);
	});

	if (capacitorapp?.addListener) {
		window.Capacitor?.Plugins?.Keyboard?.addListener('keyboardWillShow', () => {
		keyboardOpen = true;
		if (document.activeElement.classList.contains('cm-content')) {
			hideUI();
		}
	});

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

	app.workspace.on('active-leaf-change', () => {
		if ((app.workspace.activeLeaf?.view?.scroll ?? 0) === 0 || app.workspace.activeLeaf?.view?.file?.extension != "md") {
			showUI();
		}
	});
	
	app.workspace.onLayoutReady(() => {
		if (!app.mobileNavbar || !app.mobileNavbar.hideNavigation) {
			return;
		}

		realHideHeaderNav = app.mobileNavbar.hideNavigation;
		realShowHeaderNav = app.mobileNavbar.restoreNavigation;

		app.mobileNavbar.hideNavigation = function() {};	
		app.mobileNavbar.restoreNavigation = function() {};
		
		app.mobileNavbar.onScroll = function(e, t) {
			let prevScroll = this.scrollTops?.get(e) ?? 0;
			if (this.scrollTops) {
				this.scrollTops.set(e, t);
			}

			let delta = t - prevScroll;
			let atTop = t < 0.1 && prevScroll < 0.1;
			let smallJitter = Math.abs(delta) < 0.125;

			if (!atTop && !smallJitter) {
				if (delta > 0) {
					hideUI();
				} else {
					showUI();
				}
			}
		};
	});
}

Version that ignores the softkeyboard for a tiny bit when hiding/showing the UI stuff

The status bar will hide and show along with the rest of the UI stuff, including when showing/hiding the softkeyboard, but… There’s some jank when scrolling at times. No infinite loops, at least.

exports.invoke = async (app) => {
	const capacitorapp = window.Capacitor?.Plugins?.App;
	let keyboardOpen = false;
	let ignoreKeyboard = false;
	let uiStuffShown = "yes";
	let realHideHeaderNav = function() {};
	let realShowHeaderNav = function() {};

	function hideUI() {
		if (document.activeElement.closest('.metadata-property-value') || document.activeElement.closest('.metadata-property-key')) return;
		if (uiStuffShown != "no") {
			realHideHeaderNav();
			uiStuffShown = "no";
			ignoreKeyboard = true;
			setTimeout(() => { ignoreKeyboard = false }, 50);
		}
	}

	function showUI() {
		if (uiStuffShown != "yes") {
			realShowHeaderNav();
			uiStuffShown = "yes";
			ignoreKeyboard = true;
			setTimeout(() => { ignoreKeyboard = false }, 50);
		}
	}

	document.addEventListener('focusin', () => {
		if (!keyboardOpen) return;
		setTimeout(() => {
			const active = document.activeElement;
			if (!active) return;

			if (active.classList.contains('cm-content')) {
				hideUI();
			} else if (!active.closest('.metadata-property-value') && !active.closest('.metadata-property-key')) {
				showUI();
			}
		}, 100);
	});

	if (capacitorapp?.addListener) {
		window.Capacitor?.Plugins?.Keyboard?.addListener('keyboardWillShow', () => {
		keyboardOpen = true;
		if (ignoreKeyboard) return;
		if (document.activeElement.classList.contains('cm-content')) {
			hideUI();
		}
	});

		window.Capacitor?.Plugins?.Keyboard?.addListener('keyboardWillHide', () => {
			keyboardOpen = false;
			if (ignoreKeyboard) return;
			showUI();
		});
	}

	app.workspace.on('active-leaf-change', () => {
		if ((app.workspace.activeLeaf?.view?.scroll ?? 0) === 0 || app.workspace.activeLeaf?.view?.file?.extension != "md") {
			showUI();
		}
	});
	
	app.workspace.onLayoutReady(() => {
		if (!app.mobileNavbar || !app.mobileNavbar.hideNavigation) {
			return;
		}

		realHideHeaderNav = app.mobileNavbar.hideNavigation;
		realShowHeaderNav = app.mobileNavbar.restoreNavigation;

		app.mobileNavbar.hideNavigation = function() {};	
		app.mobileNavbar.restoreNavigation = function() {};
		
		app.mobileNavbar.onScroll = function(e, t) {
			let prevScroll = this.scrollTops?.get(e) ?? 0;
			if (this.scrollTops) {
				this.scrollTops.set(e, t);
			}

			let delta = t - prevScroll;
			let atTop = t < 0.1 && prevScroll < 0.1;
			let smallJitter = Math.abs(delta) < 0.125;

			if (!atTop && !smallJitter) {
				if (delta > 0) {
					hideUI();
				} else {
					showUI();
				}
			}
		};
	});
}

  1. App header and mobile navigation bar. ↩︎

1 Like

This version can still get stuck in an infinite loop if you leave Obsidian while the status bar is hidden and your softkeyboard is open. Since the status bar comes back when switching to another app, switching back to Obsidian makes the status bar hide again with the softkeyboard open.

You can always close the softkeyboard to break out of the loop, but, yeah… Kinda jank. I guess a version that always shows the status bar when the softkeyboard is open would be the best in terms of avoiding the loop.

1 Like