Pure CSS Vertical Tabs

No Plugin needed! Just sprinkle some CSS magic

Preview :framed_picture:

More images

Why Bother?

Why Vertical Tabs?

  • Because I can :sunglasses:
  • Better space utilization for wide-screen nerds

Why Not Use Plugins?

Plugins work too! But:

  1. They occupy sidebar real estate
  2. Miss out native features (like Frontmatter Title’s tab renaming feature)

Implementation :wrench:

Drop this CSS into your vault and enable the snippet:

/* Moy Vertical Tabs.css */

/* 2025-04-14 */
/* Extracted from MicroMike theme: https://github.com/ThisTheThe/MicroMike */
/* More snippets available: https://github.com/ThisTheThe/MicroMike/tree/main/Snippets */

/* Modified by @PandaNoSleep with thanks! */
/* Better experience when used with: */
/* Floating Sidebar: https://forum-zh.obsidian.md/t/topic/32602 */

/* 2025-04-16 */
/* V 2.0 */
/* New Style Settings options:
1. Vertical tab position (left/right toggle)
2. Right panel optimization: Use standard horizontal tabs by default, vertical tabs optional (right-aligned)
3. Auto-collapse tabs with fade effect
4. Width controls: Collapsed/Expanded/Max width
*/


/** Style Settings **/
/* @settings

name: Moy Vertical Tabs
id: moy-vertical-tabs
settings:
    - 
        id: align-to-right
        title: Vertical Tab align to Right
        title.zh: 靠右垂直标签页
		description: Vertical tabs are default aligned to the left, after enabling this option, vertical tabs will be aligned to the right
		description.zh: 垂直标签页默认靠左,启用这个选项后垂直标签页会靠右
        type: class-toggle
        default: false
        addCommand: true
    - 
        id: right-panel
        title: Make Right Panel Use Vertical Tabs (Right side)
        title.zh: 右侧分页的使用垂直标签栏(靠右)
		description: By default, the right panel will use the original horizontal tab to avoid taking up too much space
		description.zh: 默认情况下,右侧分页会使用原版的横向标签页,避免占用过多空间
        type: class-toggle
        default: false
        addCommand: true
    - 
        id: adjust-editor-layout
        title: Adjust Editor to the Left
        title.zh: 编辑区域偏左
		description: Adjust the layout of the editor area, slightly to the left, to balance the visual
		description.zh: 调整正文的编辑区布局,略微靠左,以平衡视觉
        type: class-toggle
        default: false
        addCommand: true
    - 
        id: collapse-header
        title: Collapsable Tabs
		title.zh: 可收缩标签栏
        type: heading
        level: 2
        collapsed: true
    - 
        id: auto-collapse-tabs
        title: Auto Collapse Tabs
        title.zh: 自动收缩标签栏
        description: Auto collapse the tabs when not in use
        description.zh: 自动收缩标签栏,鼠标悬浮展开
        type: class-toggle
        default: false
        addCommand: true
    - 
        id: tab-area-collapsed-width
		title: Tab Area Collapsed Width
		title.zh: 标签栏收缩时的宽度
        description: Set the width of the tab area when collapsed
        description.zh: 设置标签栏收缩状态的宽度(单位:px)
		type: variable-number-slider
		default: 45
        format: px
		min: 20
		max: 100
		step: 5
    - 
        id: max-width-title
        title: Set Max Width
		title.zh: 设置最大宽度
        type: heading
        level: 2
        collapsed: false
    - 
        id: max-width-info
		title: Max Width Info
        title.zh: 关于最大宽度
		description: The max-width is actually set to `min(25vw, 250px)`, so if you set the max width less than 25vw, it will actually use 25vw.\nIn the expanded state, this is the width when expanded.
        description.zh: "实际上使用的是 `min(25vw, 250px)`,所以如果你设置的最大宽度小于 25vw,那么实际使用的就是 25vw。\n展开状态下,这里设置的就是展开状态时候的宽度。"
        type: info-text
        markdown: true
    - 
        id: tab-area-max-width
        title: Tab Area Max Width
        title.zh: 标签栏最大宽度
        description: Set the width of the tab area
        description.zh: 设置标签栏展开状态的最大宽度(单位:px)
        type: variable-number-slider
        default: 250
        format: px
		min: 160
		max: 320
		step: 10

*/


	
/* --------------------------- */
/* Built-in variables (works without Style Settings) */ 
/* --------------------------- */

body {
	/* Collapsed tab width */
	--tab-area-collapsed-width: 45px;
	/* Expanded tab width */
	--tab-area-max-width: 250px;
}

/* ! Style Customization */

/* Adjust tab padding */
.workspace .mod-root .workspace-tab-header {
    /* Default: 1px 3px 3.5px */
    padding-top: 2px;
    padding-bottom: 2px;
    padding-right: 3px;
}


/* Top blank area background */
.workspace-tabs.mod-top-left-space,
.workspace-tabs.mod-top-right-space {
	background: var(--tab-container-background);
}


/* ! Set fixed width for vertical tabs */
/* Tab bar width */
.workspace .mod-root 

.workspace-tab-header-container .workspace-tab-header {
	width: var(--tab-area-max-width);
	
	max-width: min(25vw,  var(--tab-area-max-width));
}


/* Adjust editor area to left for visual balance */

.adjust-editor-layout .markdown-preview-sizer.markdown-preview-section,
.adjust-editor-layout .mod-root .cm-editor .cm-sizer {
	margin-left: max(4em, 10%) !important;
}


/* ! Title fade effect */
	
.workspace .mod-root .workspace-tab-header-container .workspace-tab-header .workspace-tab-header-inner .workspace-tab-header-inner-title {
		-webkit-mask-image: linear-gradient(to left,
											transparent 0px, transparent 5px,
											#fff 10px, #fff);
		mask-image: linear-gradient(to left,
											transparent 0px, transparent 5px,
											#fff 10px, #fff);
		mask-composite: intersect;
		-webkit-mask-composite: source-in, xor;
}

	
/* ! ------------------- ! */
/* ! FEAT: Auto-collapse tabs ! */
/* ! ------------------- ! */
.auto-collapse-tabs .workspace.is-left-sidedock-open .mod-root .workspace-tabs:not(.mod-top-right-space),
.auto-collapse-tabs .workspace .mod-root .workspace-tabs.mod-top-left-space,
.auto-collapse-tabs.right-panel .workspace.is-right-sidedock-open .mod-root .workspace-tabs:not(.mod-top-left-space),
.auto-collapse-tabs.right-panel .workspace .mod-root .workspace-tabs.mod-top-right-space
{
	.workspace-tab-header-container  .workspace-tab-header {
		width: var(--tab-area-collapsed-width, 45px);

		.workspace-tab-header-inner-title {
			white-space: nowrap !important;
			
			overflow: hidden;
			text-overflow: clip !important;

			/* margin-right: -1em; */
			margin-left: auto;
			margin-right: auto;
			
			
			left: 2px;

		}

		/* .workspace-tab-header-status-container, */
		.workspace-tab-header-inner-close-button {
			display: none;
		}
	}

	/* Hover state for collapsed tabs */
	.workspace-tab-header-container:hover .workspace-tab-header {
		width: var(--tab-area-max-width);
		
		transition: width 0.5s ease;
		transition-delay: 50ms !important; 
		
		.workspace-tab-header-inner {
			--fade-size: 0px;
		}

	}

	.workspace-tab-header-container:hover .workspace-tab-header:has(.workspace-tab-header-inner:hover) {
		.workspace-tab-header-status-container,
		.workspace-tab-header-inner-close-button {
			display: block;
		}
	}
	

	.workspace-tab-header-container:not(:hover) .workspace-tab-header {
		transition: width 0.5s ease;
		transition-delay: 220ms !important; 

	}
}


/* ------ */

/* ! ------------------- ! */
/* ! Vertical Tabs Layout ! */
/* ! ------------------- ! */


/* ! Change tab container to horizontal layout */

/* ! BASIC */
.workspace-split.mod-horizontal.mod-sidedock.mod-left-split:not(.is-sidedock-collapsed) + .mod-vertical.mod-root .workspace-tabs,
.workspace.is-left-sidedock-open .mod-root .workspace-tabs:not(.mod-top-right-space),
.workspace .mod-root .workspace-tabs.mod-top-left-space {
	flex-direction: row;
}

/* ! ALTER */
.align-to-right .workspace.is-left-sidedock-open .mod-root .workspace-tabs:not(.mod-top-right-space),
.align-to-right .workspace .mod-root .workspace-tabs.mod-top-left-space,
.right-panel .workspace.is-right-sidedock-open .mod-root .workspace-tabs:not(.mod-top-left-space),
.right-panel .workspace .mod-root .workspace-tabs.mod-top-right-space  {

	/* Reverse container layout */
	flex-direction: row-reverse;
}

.workspace-split.mod-horizontal.mod-sidedock.mod-left-split:not(.is-sidedock-collapsed) + .mod-vertical.mod-root .workspace-tabs,

.workspace.is-left-sidedock-open .mod-root .workspace-tabs:not(.mod-top-right-space),
.workspace .mod-root .workspace-tabs.mod-top-left-space,

/* Special case for single-pane right space */
/* Temporarily ignored for simplicity */
/* .workspace .mod-root:not(:has(.mod-top-left-space)) .workspace-tabs.mod-top-right-space, */

.right-panel .workspace.is-right-sidedock-open .mod-root .workspace-tabs:not(.mod-top-left-space),
.right-panel .workspace .mod-root .workspace-tabs.mod-top-right-space {

	/* Tab header container style */
	.workspace-tab-header-container {
		padding-top: 40px;
		flex-direction: column;
		justify-content: left;
		align-items: flex-start;
		gap: 5px;
		height: 100%;
		background-color: var(--background-secondary);
		padding-left: 0px !important;
		padding-right: 0px !important;

		/* Inner container style */
		.workspace-tab-header-container-inner {
			--animation-dur: 0ms !important;
			display: flex;
			flex-direction: column;
			flex-wrap: wrap;
			justify-content: flex-start;
			gap: 5px;
			margin: 0px;
			max-height: 90vh;
			overflow: scroll;
			position: relative;
			align-content: start;

		}
	}

	/* Individual tab style */
	.workspace-tab-header-container-inner .workspace-tab-header:not(.mod-active) {
		flex: 0 0 auto;
		border-radius: 6px;
		border: 2px solid var(--background-modifier-border);
		min-height: 1.6rem !important;
	}

	/* Active tab highlight */
	.workspace-tab-header.is-active {
		border-radius: 4px;
		min-height: 1.6rem !important;
		border-top: 4px solid var(--color-accent) !important;
		
		border: 1px solid var(--background-modifier-border);;
		border-bottom: 1px solid var(--background-modifier-border);;
	}

	/* Tab title style */
	.workspace-tab-header-inner-title {
		text-overflow: ellipsis;
	}

	/* Tab button style */
	.workspace-tab-header-inner {
		position: relative;
	}

	.workspace-tab-header-inner-close-button {
		position: absolute;
	}


	/* New tab button style */
	.workspace-tab-header-spacer {
		display: none;
	}

	.workspace-tab-header-new-tab {
		width: 100%;
		margin: 0px;
		padding: 0px 15px;

		justify-content: center;

		.clickable-icon {
			border: 2px solid var(--background-modifier-border);
			width: 100%;
			min-height: 1.5rem;
			border-radius: 6px;
		}
	}

	.view-header {
		border-bottom: 1px solid var(--background-modifier-border);
	}
}


/* Fix tab border issues */
.workspace-tabs:not(.mod-stacked) {

	.workspace-tab-header::before,
	.workspace-tab-header::after,
	.workspace-tab-header-inner::after {
		display: none;
	}
}

.workspace .mod-root .workspace-tab-header-inner::after {
	display: none;
}


/* Expand button */
.workspace-tab-header-tab-list {
	display: block;
	position: absolute;
	bottom: 0px;
	left: 10px;
	z-index: 10;
}

/* Right sidebar toggle */
.sidebar-toggle-button.mod-right.mod-right.mod-right {
	position: absolute;
	bottom: 0px;
	right: 5px;
}

.workspace-tab-header-tab-list,
.sidebar-toggle-button.mod-right.mod-right.mod-right {
	opacity: 0.5;
	z-index: 10;

	&:hover{
		opacity: 1.0;
	}
}

body:not(.right-panel) .workspace.is-right-sidedock-open .mod-root .workspace-tabs:not(.mod-top-left-space),
body:not(.right-panel) .workspace .mod-root .workspace-tabs.mod-top-right-space {
	.workspace-tab-header-tab-list {
		display: block;
		position: inherit !important;
		bottom: 0px;
		left: 10px;
		z-index: 10;
	}
}

Download from Github: Obsidian CSS: Vertical Tabs

Caveats :warning:

Our CSS sorcery comes with one catch:
Tab dragging doesn’t work :woozy_face:

Workarounds:

  1. Temporarily disable this CSS snippet
  2. Use plugins like Vertical Tabs for dragging

Want adjustable width? Use Style Settings plugin magic! :sparkles:

Style Settings Superpowers :superhero:

With Style Settings plugin:

  1. ↔️ Left/Right alignment toggle
  2. :magic_wand: Auto-collapse with fade effect
  3. :straight_ruler: Custom widths: Collapsed/Expanded/Max
  4. :dart: Right panel vertical tabs

250416_纯CSS实现垂直标签页-img-250416_231517

Credits :star2:

This style is essentially modified and adjusted based on sSquareUI.css from MicroMike’s Snippets, including the layout inspiration for image and text backgrounds, and so on.

If you’re interested, you can also check out the MicroMike theme:

Experimental & Ergonomic: the Micro Mike Theme - Share & showcase - Obsidian Forum


By the way, I really hope Obsidian can officially support Vertical Tabs feature :pleading_face:

4 Likes

I am working on something and I was up to 35 tabs.
This came in handy.

It’s fantastic that you added a Style Settings feature to this.

Keep up your shares!

Regards,

Y.

1 Like

I made a pseudo-plugin that toggles your snippet.
Code:

import { Plugin, App, Notice } from "obsidian";

async function toggleVerticalTabs(app: App): Promise<void> {
    try {
        const snippetName = "vertical-tabs-by-Moy";
        const customCss = app.customCss;

        if (!customCss) {
            throw new Error("Custom CSS API not available");
        }

        // Check if snippet exists
        const snippetExists = await app.vault.adapter.exists(
            `${app.vault.configDir}/snippets/${snippetName}.css`
        );

        if (!snippetExists) {
            throw new Error(`Snippet '${snippetName}.css' not found`);
        }

        // Get current state and toggle it
        const isEnabled = customCss.enabledSnippets.has(snippetName);
        customCss.setCssEnabledStatus(snippetName, !isEnabled);

        // Show notification
        new Notice(
            `Vertical tabs ${!isEnabled ? "enabled" : "disabled"}`
        );

        console.log("Vertical tabs toggled:", {
            wasEnabled: isEnabled,
            nowEnabled: !isEnabled
        });

    } catch (error) {
        console.error("Error toggling vertical tabs:", error);
        new Notice(`Failed to toggle vertical tabs: ${error.message}`, 5000);
    }
}

export class SwitchVerticalTabsPlugin extends Plugin {
    async onload() {
        this.addCommand({
            id: "switch-vertical-tabs",
            name: "Toggle Vertical Tabs",
            callback: () => toggleVerticalTabs(this.app),
        });
    }
}

export async function invoke(app: App): Promise<void> {
    return toggleVerticalTabs(app);
}

Save code as Toggle-Vertical-Tabs.ts or something (doesn’t matter actually but you’ll need to find the script by the name you set when assigning hotkeys) and in the const snippetName = "vertical-tabs-by-Moy"; line add the name of the snippet you saved it with (this part is more important).

You’ll need the CodeScript Toolkit plugin that will automatically load the .ts file, you just need to add in the settings, which folder it will look for, as I showed in this post with screenshots.

So now I have a key combo bound to the script and it toggles the snippet ON and OFF.
Pretty cool.

1 Like

Heyyy! Thanks a bunch for replying—honestly, this forum can sometimes feel quite quiet. I’ve gotten used to just talking to myself here… So your response means a lot :smiley:

P.S. Your script is :fire:!

Let me share my not-so-secret weapon: the MySnippets plugin.

It’s been a game-changer for CSS tinkering.
That’s how it looks like:

It simply lists all of your CSS snippets and provides switches for them.
And it’s also easily to edit or create CSS snippet with it :wink:

1 Like

Quiet…is a five-letter word…yes. :slight_smile: I was thinking more along the lines of a four-letter word. :slight_smile:

Yeah, plugins are nice, but currently I can’t think of a snippet I’d want to toggle on demand apart from your one…
I agree if one wants to be able to toggle more than one it would be stupid to use my script 4-5 times over.
I’ll have a look at the plugin.

I also considered adding a “Disable Vertical Tabs” option to the Style Settings, but as you can see, there are two toggleable options present (right-panel and align-to-right), and it’s kinda complex already, so after a moment, I decided against it, haha.

1 Like

Finally got time to dive into the CodeScriptToolkit, thanks for your guidance!

I noticed that you also have a habit of using many scripts in Obsidian (from the threads you sent) - me, too!

SO, Ahem, I know you’ve been used to this plugin, and 99% not willing to change - but I’d like to recommend another plugin: Note toolbar (NTB)

It supports directly calling Templater scripts( Execute Templater command/file ), so I’ve started to transfer many of my TP Scripts into it.

  • Create toolbars with items that link to commands, files/folders, URIs/URLs, menus, and scripts (Dataview, JS Engine, Templater, and built-in support for JavaScript).

Besides, it’s a very useful toolbar plugin - it can display different toolbars based your note (or folder), which I think is crucial for experienced Obsidian user.

Thanks again for sharing, it’s a pleasure to communicate with you *tips hat*

1 Like

I didn’t look deeply enough into Note Toolbar yet.
To be honest, anything to with scripts in the toolbar plugin would make most sense to me if the results would be added (automatically, upon entering a note, such as a daily) to its own modal, rather than in the corresponding file “underneath”. This thread was beginning to explore such implementations but didn’t gather much interest yet:

Otherwise,…

  • When I can trigger scripts from keyboard shortcuts (on PC), I wouldn’t want to look for a script in a toolbar (on mobile this would make more sense)
  • I try to limit my use of Templater now to note creation process template files only

But if you have anything cooked up, you are welcome to Share & showcase it.

Hi, thank you for your CSS, but I recently decided to try to use your css and for some unknown reason, it doesn’t work at all. I mean, in the Style Settings plugin settings I set the right side and change the width, but after that nothing happens. How can this be fixed or what could be the reason for something like this?

It’s possible you are using a theme (Minimal?) with some settings that hides the tabs or UI elements?
Try a different theme and if it works there, go back to the current theme to investigate.