Universal Tabs for Sheets and More Conditional CSS

This is a massive post. If you just want the Universal Tabs version, jump down to that heading. But I hope you get some use out of the build-up – I spent a lot of time writing it!

Imagine you want to create tabs: sections of a sheet where you click a button (or a checkbox) and part of the sheet is hidden or becomes visible.

Maybe you have several types of sheet in the same one (like a character sheet, an NPC sheet, an Army Sheet, a Ship Sheet, and more), or maybe you have separate tabs within one sheet (a character sheet that contains a separate tabs for Basic Stats, Skills & Attacks, Spells, Gear, etc.). or maybe you have a Settings popup, or maybe within your Spells tab you have separate tabs for each class or level.

When you show one tab something else is usually hidden, but this isn’t required. Fpr example, when you show the Character Sheet, the NPC, Army, and Ship Sheets are hidden (you only want one of them visible at a time). But when you pick the Settings tab, you might want it to hover over the rest of the sheet. Likewise, picking a spell class may hide other spell classes but leave the rest of the Spells tab visible.

I’d say only the very simplest sheets avoid using tabs. If a sheet has a page 1 and a page 2, you probably want this technique.

The CSS Wiki describes several techniques for doing this. I’ll describe a technique I have found very reliable, which is easily portable between sheets. It requires HTML, CSS, and JavaScript, but the steps will be described below, in simple terms.

CSS: Conditional Selectors

This requires a rudimentary knowledge of CSS Selectors. Don’t panic, this is pretty straightforward.

You’ll have a HTML selection like:

<div class="character-sheet">
   <div class="bio">
       <h4> BIO </hv>
       <span>Character name</span>
       <input type="text" name="attr_character_name" value="">
   </div>
</div>Code language: HTML, XML (xml)

Things like div, h4, span, and input are Elements, the name for a HTML thing (while class, type, name, and value are Attributes – the properties inside a HTML Element.

In CSS, any of those elements or classes can be a selector. Like:

div {
  border: 1pt solid black;
}
div div.bio span {
  color: blue;
}Code language: CSS (css)

Here we have two CSS declaration blocks. The first says that all divs should have a solid black border (the div is the selector). When they are multiple things you have yto break them down – each subsequent item must be inside the earlier ones. So it’s easiest to right right to left. div div.bio span means this selector targets spans, that are inside divs with a class of bio, that are themselves inside divs. It then says that in any matching region, the words (their color) should be blue.

The first line of each declaration block is the selector. Here we have a variation of the original HTML above, and we want to hide the entire bio block.

<div class="character-sheet">
   span>Show Bio</span>
   <input type="checkbox" name="attr_show_bio" value="1">
   <input type="hidden" name="attr_show_bio" class="show-bio">
   <div class="bio">
       <h4> BIO </hv>
       <span>Character name</span>
       <input type="text" name="attr_character_name" value="">
   </div>
</div>Code language: HTML, XML (xml)

There is something curious to mention here – namely, the use of the hidden input. We’ll come back to that.

Now we need a CSS block that shows and hides the bio block based on the condition of the checkbox.

input.show-bio:not([value="1"]) ~ div.bio {
    display:none;
}Code language: CSS (css)

This selector (the first line) can be split into three parts.

First, we look at the part below the ~ symbol. We are working on any input with the class "show-bio". We see it declare as a value="1" and the :not() statement changes that to any value that is not 1.

Then the ~ symbol says to look for anything that matches the last selector, in the same location in the HTML hierarchy. It cannot be inside another container, and must be after the first selector.

Putting it together, this says, “if an input with a class "show-bio" has a value anything other than 1, then hide any divs with a class bio in the same region.”

The :not() rule

Using :not() simplifies our code. All elements have a default status, and that is never display:none. This means that we don’t need to create a rule for showing the element – we only need a rule for hiding it. When we are checking for multiple things (not just 1 or 0), using not means we can check for anything that doesn’t match our desired value.

Why Do We Need a Hidden Input?

This a limitation of Roll20. The CSS value of inputs is only read if the input is hidden. Hidden inputs are special in this way. A hidden input is always, well, hidden (given a display:none property), and is dynamic, so when doing conditional CSS they are used a lot.

When two inputs have the same name, they automatically have the same value, so we can easily have a visible checkbox and a hidden input – just give them both the same name=.

Why Not Use the Checkbox?

The CSS does respond to the :checked state of a checkbox, but the examples below depend on the value of the checkbox (or any other kind of input). Since we are using value, we need a hidden input.

Conditional CSS Blocks

For simple situations like the above, we don’t need a sheet worker. But some of our examples below are more complicated, so it’s handy to know how to use the sheet worker approach.

We are going to look at three common but different situations where you want to use conditional CSS blocks, with the HTML, CSS, and Javascript for each then we’ll see single universal solution for all three situations – a solution that will handle the vast number of situations you need.

Toggles – the basic Action Button

There is a sheet worker technology in Roll20 that is perfect for tabs: the Action Button. You can use these as described for checkboxes above but you might find the appearance of action buttons easier to match your design. For example, we start with basically the same HTML as before, but replace the checkbox with an action button.

<div class="character-sheet">
   span>Show Bio</span>
   <button type="action" name="act_show_bio">Bio</button>
   <input type="hidden" name="attr_show_bio" class="show-bio">
   <div class="bio">
       <h4> BIO </hv>
       <span>Character name</span>
       <input type="text" name="attr_character_name" value="">
   </div>
</div>Code language: HTML, XML (xml)

We still need the hidden input which must have a class assigned. We still need essentually the same CSS block.

input.show-bio:not([value="1"]) ~ div.bio {
    display:none;
}Code language: CSS (css)

The new part is the Sheet Worker.

on('clicked:show_bio', () => {
    getAttrs(['show_bio'], values => {
        const show_bio = +values.show_bio || 0;
        setAttrs({
            show_bio: 1- show_bio
        });
    });
});Code language: JavaScript (javascript)

The first line tells is this is triggered when a button named show_bio is clicked. The getAttrs line looks at the character sheet and finds the attribute named show_bio. It’s important to realise that the action button and the hidden input have the same name, but they are different items (one using act_show_bio, the other attr_show_bio). This isn’t important here, but will be important later.

Inside the setAttrs we get a new value of 1- the old value. Now the old value can only be 0 or 1, so te new value is always 1 or 0: this is a toggle. It can only have two values. This is very like setting a checkbox (which is the :checked or :not(:checked) states: exactly 2 states.

Inserts – more extravagant Action Buttons

The method described just replaces checkboxes, but we are about to see how this same technique can be expanded on.

Let’s say we start with three tabs – one for character sheet, one for journal, and one for NPC stats, and set them up like this:

The HTML

<input type="hidden" name="attr_sheet_tab" class="tabs" value="character">
<div>
    <button type="action" name="act_character">Character</button>
    <button type="action" name="act_journal">Journal</button>
    <button type="action" name="act_npc">NPC</button>
</div>
<div class="character">
    <span>Show Bio</span>
    <button type="action" name="act_show_bio">Bio</button>
    <input type="hidden" name="attr_show_bio" class="show-bio" >
    <div class="bio">
        <h4> BIO </hv>
        <span>Character name</span>
        <input type="text" name="attr_character_name" value="">
    </div>
</div>
<div class="journal">
    Journal
</div>
<div class="npc">
    NPC
</div>Code language: HTML, XML (xml)

Note that the earlier created Bio is inside the character sheet and it still works – we can have a lot of elements inside each of the three divs, and can have many more divs.

Note that we start with a separate div for the sheet tab buttons – they can be placed literally anywhere (and we’ll shortly see a way to get rid of them all and use a select dropdown).

The important thing is that hidden input: sheet_tab. It must be placed in the same “level” as the divs we are showing and hiding. It can be placed somewhere else, but that requires different CSS.

CSS

And here we have the CSS (and we’ll see how useful that :not() rule is).

.tabs:not([value="character"]) ~ .character,
.tabs:not([value="npc"]) ~ .npc,
.tabs:not([value="journal"]) ~ .journal {
    display: none;
}Code language: CSS (css)

Here we have three declaration blocks- you can use a comma to separate multiple declarations, and each does the same thing, It first checks the selector before the ~ symbol: it looks for the value of the item with class "tab". Then any item that does not match that value set to display:none.

So when value="npc", the character and journal divs don’t match so they are set to display:none.

The CSS is honestly the most tedous part to write out! It’s also often the hardest to mentally figure out, so feel free to study that.

Now we need a way to set the value of the sheet_tab attribute. That’s where the action buttons and sheet workers come in.

JavaScript

We effectively need one sheet worker per possible tab – here three, but there can be many. As it turns out, there’s a way to simplify that to a single sheet worker no matter how many tabs. But we’ll look at the individual version first:

Inside our script block, we’d need to create something like this:

   on('clicked:character', function() {
      setAttrs({
         sheet_tab: 'character'
      });
   });Code language: JavaScript (javascript)

This detects when the character button is clicked, and change the sheet_tab value to character. That’s exactly what we want, but we want to avoid manually creating multiple sheetworkers. So we can do this:

    const buttonlist = ["character","journal","npc"];
    buttonlist.forEach(button => {
        on(`clicked:${button}`, function() {
            setAttrs({
                sheet_tab: button
            });
        });
    });Code language: JavaScript (javascript)

The only thing we need to edit here is the first line: we want an array of button names. Recall that each button name is also the same as the desired value of the sheet tab. So this bit of code responds to a button being clicked and sends that same text to the sheet_tab attribute. That’s exactly what we want.

You can find a more basic version of this tip on the Roll20 Wiki’s CSS Wizardry page, written by Finderski and me. The concept isn’t original though – this is just how JS and CSS work on Roll20.

Watching Attributes for Changes instead of using Action Buttons

You can duplicate the effect of the sheet worker without using action buttons. One way is to use a select dropdown with the same name as the hidden input. You still need the hidden input. When both have the same name, the hidden input is always equal to the select which is perfect for our CSS.

<input type="hidden" name="attr_sheet_tab" class="tabs" value="character">
<select name="attr_sheet_tab">
    <option selected>character</option>
    <option>journal</option>
    <option>npc</option>
</select>Code language: HTML, XML (xml)

You can place the select or a copy of it anywhere, but the hidden input must remain where it is. You don’t need extra copies of it.

When Not To Use Not

This dropdown used with the same CSS changes which sheet is visible. You don’t just have to use it to make a sheet visible or not. You could just as easily do:

.background[value="red"] ~ .character {
    background-color: red;
}
.background[value="green"] ~ .character {
    background-color: green;
}
.background[value="blue"] ~ .character {
    background-color: blue;
}Code language: CSS (css)

Here we have a set of action buttons or a select for setting the background of the element with the class "character". Since we are targeting one specific element, we don’t use :not(). The HTML would look something like this (if using a select):

<input type="hidden" name="attr_background" class="<strong>background</strong>" value="green">
<select name="attr_background">
    <option selected>green</option>
    <option>red</option>
    <option>blue</option>
</select>Code language: HTML, XML (xml)

A Settings Pane

For our final example, it’s a common desire to have a config or settings pane that appears floating in top of the rest of the character sheet. This might be triggered with a checkbox or an action button. This HTML triggers an action button which toggles the hidden input’s value between 1 and 0.

<button type="action" name="attr_show_settings">Settings</button>
<input type="hidden" name="attr_show_settings" class="<strong>show_settings</strong>" value="0">
<div class="settings">
    Usually Hidden Settings frame
</div>Code language: HTML, XML (xml)

To make the settings pane visible, we’d need to set its z-index higher than the rest of the sheet. We probably also want to use position: absolute to make it appear where we want, outside the normal flow of the sheet. This CSS sets the settings frame 50px in from the left of the sheet, and 20px down from the top of the sheet.

.settings {
  position: absolute;
  left: 50px;
  top: 20px;
  z-index: 99;
}
.show_settings:not([value="1"]) ~ .settings {
  display: none;
}Code language: CSS (css)

This approach can be used for other things. In some of my games, I use this technique to create a hidden frame for tracking experience and skill purchases. These are things never used during a session, only at the end of adventures, so it’s nice to have a panel the players can show only when needed.

Universal Tabs

The code below duplicates the function of the action buttons, and can be used for all action buttons in a sheet where you want to show, hide, or change the appearance of a HTL element.

Copy the code below into your script block. It might look fiendishly complicated, but you can ignore most of it. The only part to modify is the tabs variable at the top. There are instructions and examples below the code.

    /* === TAB CODE: DO NOT ALTER === */
    const run_tabs = tabs => {
       const events_for_tabs = (stats, clicks = 0, open = 0, section = '') =>
           stats.reduce((all, one) => 
               `${all} ${clicks ? 'clicked' : 'change'}:` + 
               `${one}`, `${open ? 'sheet:opened ' : ''}`);

       /* build event line */
       const tab_inserts = Object.keys(tabs.inserts).reduce((all, one) => 
               [...all, ...tabs.inserts[one] ], []);
       const tab_events = `${events_for_tabs(tabs.toggles, 1)} ${events_for_tabs(tab_inserts, 1)}`;

       /* build a getAttrs line for the attributes you need */
       const tab_stats = [...tabs.toggles, ...Object.keys(tabs.inserts)]; 
       on(tab_events, eventInfo => {
           getAttrs(tab_stats, values => {
               const trigger_full = eventInfo.triggerName.split(":");
               const trigger = trigger_full[1];
               output = {};
               if(tabs.toggles.includes(trigger)) {
                   output[trigger] = 1 - (+values[trigger] || 0);
                   // the click event and attribute name must be the same.

               } else if(tab_inserts.includes(trigger)) { 
                   /* Check if the trigger is in any of the arrays listed here
                      if it is, find the name of that array, set it to destination
                      and save the trigger.
                      Watch out for typos!
                   */
                   Object.keys(tabs.inserts).forEach(insert => {
                       if(tabs.inserts[insert].includes(trigger)) {
                           const destination = insert; // the value here is the location to save the trigger.
                           output[destination] = trigger;
                       }
                   });
               }

               if(Object.keys(output)) {
                   setAttrs(output);
               }
           });
       });
    };
    /* === TAB CODE END === */
    const tabs = {
        toggles: [],  
            /* an array of attributes you just want to toggle */
        inserts: {
            /* action buttons which insert the name in a specific location */
        }
    };
    run_tabs(tabs);Code language: JavaScript (javascript)

This code looks complicated, but you copy that entire thing into your script block and edit only the const tab part. Here’s an example, which presumes HTML is created for action buttons..

const tabs = {
   toggles: ['show_bio', 'show_settings'],  
   inserts: {
      sheet_tab: ['character', 'journal', 'npc'],
      background: ['red', 'green', 'blue']
   }
};Code language: JavaScript (javascript)

Everything else is in a run_tabs function to isolate the needed code and variables from the rest of the sheet.

You’ll also need to create the CSS Declaration blocks. The code for them is already on this page.

Issues With Repeating Sections

This code is not designed for inside repeating sections. The code for those is a bit more complicated.

In Summary

This article includes a Sheet Worker that can be used for as many tabs and other bits of conditional CSS as you want. For each bit of conditional code, you need:

  • HTML with a hidden input which has a CSS class (vital)
  • The CSS which describes the change you want.
  • Either action buttons which use a sheet worker to set the hidden input’s value, or another way to set that value (common approaches are a checkbox or select dropdown).

You need all of those to make your tabs or other conditional CSS work. The Universal Tabs sheet worker on this page makes the action buttons easy. You’ll have to write the CSS yourself.

1 thought on “Universal Tabs for Sheets and More Conditional CSS

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.