Skip to content

Instantly share code, notes, and snippets.

@sailKiteV
Last active April 21, 2023 14:04
Show Gist options
  • Save sailKiteV/e1124244161e2ab5ac3045562beee4ae to your computer and use it in GitHub Desktop.
Save sailKiteV/e1124244161e2ab5ac3045562beee4ae to your computer and use it in GitHub Desktop.
A rather inelegant—but functional!—attempt at providing a callout-based, pure CSS implementation of in-leaf tabs for Obsidian.md

Mostly-CSS Tabs in Obsidian


⚠ Live Preview users, please see below! ⚠

This is an implementation for a tab-based rendering scheme in Obsidian.md, leaning heavily on capabilities offered by the native callouts feature of the app. The snippet is available in this gist or at this link. We use mostly CSS and a very minor amount of inline HTML to get everything working properly. To get started, you'll want to be familiar with the basic syntax of callouts.

The Callouts

We will be using two types of callout to help us with structure, and a third type to hold our desired content:

  • [!tabbed-box] is the outermost callout and will serve as a container for any tabs and content that you want to have grouped together.
  • [!tabs] is the mid-level callout, and will hold all of the content that you want to divide between your various tabs. This one is required for formatting purposes.
  • [!tab-content] is a reusable innermost callout that holds the actual markdown content that you want to see in each tab.

The basic structure inside the actual note will look something like this:

> [!tabbed-box]
> %% input declarations go here %%
> %% label declarations go here %%
>
> > [!tabs]
> > > [!tab-content]
> > > This is stuff in the first tab.
> >
> > > [!tab-content]
> > > These are things in the second tab!
> > > The second tab is cozy.  

Overall, there's nothing too complex here from a callouts perspective, just a lot of nesting which can be somewhat uncomfortable sometimes.

The HTML

However, we do have an important step that we still need to take care of, and that's our inputs and labels to make the whole thing tick. All of our CSS is based on hiding radio buttons and prettying-up some HTML label tags that we can refer to with our CSS selectors. Let's see what a declaration for a single input and label would look like:

> <input type="radio" id="first1" name="tab-group-1" checked>
> <label for="first1">The First Label Title!</label>

Without getting too deep into the HTML explanation of what all this means, there are a few bits to be cognizant of:

  • id="first1" is an example of a radio button's internal ID and is required to be unique within any single note. For syntax reasons, each ID within a given group must end with the number of its index, such as 1, 2, 3, etc. More examples of valid IDs would be id="MyTab1" for a first tab's label, id="portrait-2" for a second tab's label, and so on. The crucial feature here is that the ID ends with the number that corresponds to its place in the order of the group.
  • name="tab-group-1" is the name of the group to which an input belongs. These can be named anything you like, as long as all inputs within the same group have the same name, and each group's name is different.
  • for="first1" is a property that connects our styled label to the underlying radio button input. Notice that our for value here is the same as our example id above. These must be the same in order to bind the label to the input.
  • The First Label Title! is the actual text content that will render inside the tab label, and can be whatever you want.

And that's really all that we need on the HTML side. If you want a full demo to read through, check out example.md.


Live Preview

As of Obsidian v0.14.6, this snippet will not fully work in Live Preview. It will partially work in editing view, and will only work in reading view if the pane in which a given note is opened in has not yet been in editing view at all. This means that in order to have any reasonable functionality of the snippet in Live Preview, you must also have your default new pane view be reading mode. If this requirement is not met, then the labels rendered in reading mode will not correctly bind to their hidden radio button inputs, preventing you from changing tabs.

Additionally, Live Preview's parsing of note markdown can interfere with correct operation of the snippet even with the above prerequisites satisfied. In order to guarantee that a given tab group will function, you must essentially sanitize the preceding indents. Here is an example:

> >

> [!tabbed-box]
> etc.

If you don't want to have to see those extra indents and such in reading view, you can comment them out like so:

%%> >%%

> [!tabbed-box]
> etc.

Congratulations! Now you should have at least a little bit of proper functionality in Live Preview.

> [!tabbed-box]  
> <input id="1st1" type="radio" name="tab-group-1">
> <input id="1st2" type="radio" name="tab-group-1">
> <input id="1st3" type="radio" name="tab-group-1">
> <label for="1st1">Oh</label>
> <label for="1st2">My</label>
> <label for="1st3">Gosh</label>
> 
> > [!tabs]  
> > > [!tab-content]
> > > [[Useful ALT Codes]] #woohoo
> >
> > > [!tab-content]  
> > > | it    | all     |
| ----- | ------- |
| works | inside! |
> > 
> > > [!tab-content]  
> > > > [!info] title  
> > > > "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."  
/* --- Attempted Tab Implementation Through Callouts --- */
/* initial attribute setup */
div[data-callout="tabbed-box"],
div[data-callout="tabs"],
div[data-callout="tab-content"] { background: unset; border: unset; margin: unset; padding: unset; clear: both; overflow: hidden; }
div[data-callout="tabbed-box"].callout > .callout-title,
div[data-callout="tabs"].callout > .callout-title,
div[data-callout="tab-content"] > .callout-title { display: none; }
div[data-callout="tabbed-box"] > .callout-content,
div[data-callout="tabs"] > .callout-content,
div[data-callout="tab-content"] > .callout-content {
border: none !important;
}
/* fix margin in Live Preview Edit Mode */
.markdown-source-view.mod-cm6 .callout:is(div[data-callout="tabbed-box"], div[data-callout="tabs"], div[data-callout="tab-content"]) {
margin: 0px;
}
/* baseline label style */
div[data-callout="tabbed-box"] label {
background: var(--background-primary);
padding: 10px;
top: 10px;
margin-left: -1px;
left: 0px;
}
/* "default" and checked label style */
/*[type=radio]:is(:checked, [checked]) + label*/
[type=radio][id$="1"]:is(:checked, [checked]) ~ label:nth-of-type(1),
[type=radio][id$="2"]:is(:checked, [checked]) ~ label:nth-of-type(2),
[type=radio][id$="3"]:is(:checked, [checked]) ~ label:nth-of-type(3),
[type=radio][id$="4"]:is(:checked, [checked]) ~ label:nth-of-type(4),
[type=radio][id$="5"]:is(:checked, [checked]) ~ label:nth-of-type(5),
[type=radio][id$="6"]:is(:checked, [checked]) ~ label:nth-of-type(6),
label:first-of-type:not(input:is(:checked, [checked]) ~ label) {
background: var(--background-active-tab);
z-index: 2;
}
/* hide radio buttons */
div[data-callout="tabbed-box"] > .callout-content > [type="radio"] {
display: none;
}
div[data-callout="tabbed-box"] > .callout-content {
--background-active-tab: #222229;
padding: 0;
padding-top: 5px;
}
div[data-callout="tabs"] {
padding-top: 5px;
}
div[data-callout="tabs"] > .callout-content {
display: grid;
grid-template-columns: 1fr;
padding: 0;
}
/* stack content areas in grid */
div[data-callout="tab-content"] {
grid-column-start: 1;
grid-column-end: 1;
grid-row: 1;
}
/* hide inactive content */
div[data-callout="tab-content"] {
z-index: 1;
display: none;
}
div[data-callout="tab-content"] > .callout-content {
background-color: var(--background-active-tab);
}
div[data-callout="tab-content"] > .callout-content > p:nth-child(1),
div[data-callout="tab-content"] > .callout-content > table:nth-child(1) {
margin-top: 10px;
}
/* ultra ugly list of label-to-active-tab relations */
input[type=radio]:last-of-type:not( input[type=radio]:checked ~ input[type=radio], :checked, [checked] ) ~ div[data-callout="tabs"] > .callout-content div[data-callout="tab-content"]:nth-of-type(1),
div[data-callout="tabbed-box"] > .callout-content > [type=radio][id$="1"]:is(:checked, [checked]) ~ div[data-callout="tabs"] > .callout-content div[data-callout="tab-content"]:nth-of-type(1) {
z-index: 2;
display: block;
}
div[data-callout="tabbed-box"] > .callout-content > [type=radio][id$="2"]:is(:checked, [checked]) ~ div[data-callout="tabs"] > .callout-content div[data-callout="tab-content"]:nth-of-type(2) {
z-index: 2;
display: block;
}
div[data-callout="tabbed-box"] > .callout-content > [type=radio][id$="3"]:is(:checked, [checked]) ~ div[data-callout="tabs"] > .callout-content div[data-callout="tab-content"]:nth-of-type(3) {
z-index: 2;
display: block;
}
div[data-callout="tabbed-box"] > .callout-content > [type=radio][id$="4"]:is(:checked, [checked]) ~ div[data-callout="tabs"] > .callout-content div[data-callout="tab-content"]:nth-of-type(4) {
z-index: 2;
display: block;
}
div[data-callout="tabbed-box"] > .callout-content > [type=radio][id$="5"]:is(:checked, [checked]) ~ div[data-callout="tabs"] > .callout-content div[data-callout="tab-content"]:nth-of-type(5) {
z-index: 2;
display: block;
}
div[data-callout="tabbed-box"] > .callout-content > [type=radio][id$="6"]:is(:checked, [checked]) ~ div[data-callout="tabs"] > .callout-content div[data-callout="tab-content"]:nth-of-type(6) {
z-index: 2;
display: block;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment