Tabbed user interfaces have been with us now for more than two decades. Many of you will remember the old TabPro custom control, shown below in Figure 1a, that added tabs to many ASNA Visual RPG Classic applications back in the day.

Figure 1a. TabPro COM tab control.

In the late 90s and early 2000s, Amazon helped pioneer tabbed UIs in HTML. However, what started as a rational set of humble tabs later gave way to a Godzilla-like lumbering beast of tabs.

Figure 1b. Amazon's Tabbed Godzilla! (circa 2000)

In its early days, Amazon's use of tabs was an inspiration to many of us. But as Amazon's tabbed Godzilla grew out of hand, it quickly became the example of what not to do with tabs. While Amazon has removed most tabbed use from its site, the UI as a navigation metaphor persists and is still used on many Web sites.

Creating an HTML tabbed UI with ASNA Visual RPG

Adding a tabbed UI to ASNA Visual RPG (AVR) fat client Windows apps is as easy as using the .NET-provided TabControl and its tabbed pages. There used to be an ASP.NET equivalent control for Web pages, but that control has long been deprecated. For the longest time, creating the illusion of tabs in a Web page meant using HTML tables, creating images for each tab, writing hundreds of lines of JavaScript and CSS. This used to be quite a chore--and it rarely worked exactly as desired. In 2003, A List Apart published a revolutionary, for the time, "sliding doors" CSS technique that helped; but even with that technique, tabs remained quite challenging. It's also possible to create a tabbed UI of sorts with clever use of ASP.NET servers-side buttons and panels. While potentially workable, this technique required lots of code to manage the tab presentation and was kludgy at best.

CSS to the rescue

As CSS matured, we learned we could build tabbed structures from plain ol' HTML markup. For example, consider this HTML unordered list:

<div class="container">
    <ul class="tabs">
        <li class="tab-link current" data-tab="tab-1">Tab One</li>
        <li class="tab-link" data-tab="tab-2">Tab Two</li>
        <li class="tab-link" data-tab="tab-3">Tab Three</li>
        <li class="tab-link" data-tab="tab-4">Tab Four</li>
    </ul>

    <div id="tab-1" class="tab-content current">
        Tab1
    </div>
    <div id="tab-2" class="tab-content">
         Tab2
    </div>
    <div id="tab-3" class="tab-content">
        Tab3
    </div>
    <div id="tab-4" class="tab-content">
        Tab4
    </div>
</div>        

Figure 2a. HTML unordered list.

The HTML above, without a CSS applied to it, displays as shown in Figure 2b below:

Figure 2b. Figure 2a's HTML rendered in a browser without any CSS.

The UI in Figure 2b hardly looks like a tabbed UI, but when you apply the CSS below in Figure 2c, a nearly magical transformation occurs.

body{
    margin-top: 100px;
    font-family: 'Trebuchet MS', serif;
    line-height: 1.6
}
.container{
    width: 800px;
    margin: 0 auto;
}

ul.tabs{
    margin: 0px;
    padding: 0px;
    list-style: none;
}
ul.tabs li{
    background: none;
    color: #222;
    display: inline-block;
    padding: 10px 15px;
    cursor: pointer;
}

ul.tabs li.current{
    background: #ededed;
    color: #222;
}

.tab-content{
    display: none;
    background: #ededed;
    padding: 15px;
}

.tab-content.current{
    display: inherit;
}

Figure 2c. CSS to render Figure 2a's HTML as a tabbed UI.

When Figure 2a's HTML is rendered with Figure 2c's CSS, the UI is transformed into a graceful tabbed UI. You can see the UI presented here in this CodePen.

Figure 2d. Figure 2a rendered with Figure 2c's CSS.

If you aren't famliar with CodePen, you should be! Read more about it here.

While these early CSS/HTML transformation techniques were a huge step forward, they had two drawbacks:

  • They weren't responsive. They rendered well on a desktop, but often have rendering issues on mobile devices.
  • They required JavaScript (and usually jQuery) to govern the presentation. You're probably going to need JavaScript anyway, but if you can remove the need for it to manage the presentation layer (and let it focus on managing the logic), that's a good thing. The more JavaScript you need, the more things you need to worry about.

While there isn't much JavaScript in the CodePen example above (nine lines), that JavaScript is minimal. It doesn't fire an event when the focused tab is changed nor does it help with persisting the selected across ASP.NET postbacks. If you are a jQuery fan, there is a lot that can be done with the technique provided here. However, an even better way lurks.

A better approach with CSS Flex

A relatively new CSS feature, called FlexBox (or just Flex) has been added that dramatically aids positioning HTML elements. Browser support for older browsers, is a potential issue, so beware. Your biggest exposure (as always!) is IE. CSS Flex is supported in FireFox, Chrome, Safari, MS EDGE, and, to a degree in IE 11. Support is generally broad enough for Flex that you can probably start using it today--especially for mobile apps. There are minor kinks yet to be worked out in Flex, but its core use cases are ready to use. You can read more about CSS Flex here. As always, test your code thoroughly on any potential end-user devices (not just emulators, real devices!).

Note there is also an emerging CSS feature called Grid on the horizon. Don't confuse Flex with Grid. The example presented here uses Flex.

As I pondered revisiting HTML tabs (for what seems like the 287th time), I found two Flex-based examples that are really great. The first example, by a coder named Stephan Barthel, from this CodePen, isn't responsive, and that may be an issue--but if you have a small fixed number of tabs, it's a great solution. If your tabs grow (and they shouldn't grow much, as Amazon learned!), a second example, by a coder named Josh Vogt, from this CodePen, is very interesting. I'm using the first code from the first CodePen example for the foundation of this article's technique (mostly because its CSS is shorter and easier to understand). However, adopting the responsive example's CSS later would be easy to do and wouldn't require changes to the markup or JavaScript.

Neither of these two examples require any JavaScript for the presentation. We'll need to add a little JavaScript for managing state, but it's great that we don't need any JavaScript for the tabs presentation. These examples also use radio buttons and labels instead of an unordered list as the tab foundation. Radio buttons? Yep. Consider the HTML shown below in Figure 3a (this HTML is from the first CodePen example referenced above).

<section>
    <input id="tab-one" type="radio" name="grp" checked="checked"/>
    <label for="tab-one">Tab One</label>
    <div>
      Tab 1 contents
    </div>
    
    <input id="tab-two" type="radio" name="grp" />
    <label for="tab-two">Tab Two</label>
    <div>
      Tab 2 contents
    </div>
    
    <input id="tab-three" type="radio" name="grp" />
    <label for="tab-three">Tab Three</label>
    <div>
      Tab 3 contents
    </div>
</section>

Figure 3a. HTML for CSS tabs with radio buttons and labels.

In Figure 3a above, the input/label tags identify a tab and its text, and the adjoining div tag defines the tabs' content area; therefore, three tabs are defined in Figure 3a. As you can probably tell, that markup doesn't look much like it renders as tabs, does it? It will!

The CSS below in Figure 3b (again, taken directly from the first CodePen example) renders the HTML in Figure 3a above as tabs.

body {
    font-family: Arial;
    color: #333;
}
section {
    display: -webkit-flex;
    display: flex;
    -webkit-flex-wrap: wrap;
    flex-wrap: wrap;
}

label {
    background: #eee; 
    border: 1px solid #ddd; 
    padding: .7em 1em;
    cursor: pointer;
    z-index: 1;
    margin-left: -1px;
}

label:first-of-type {
    margin-left: 0;
}

div {
    width: 100%;
    margin-top: -1px;
    padding: 1em;
    border: 1px solid #ddd;
    -webkit-order: 1;
    order: 1;
}

input[type=radio], div {
    display: none;
}

input[type=radio]:checked + label {
    background: #fff;
    border-bottom: 1px solid #fff;
}

input[type=radio]:checked + label + div {
    display: block;
}

Figure 3b. CSS to render Figure 3a's HTML as a tabbed UI.

When the CSS above in Figure 3b is applied to Figure 3a's HTML, the UI is rendered in the browser as shown below in Figure 3c. It's rendered simple, clean, and without any JavaScript.

Figure 3c. Figure 3a's HTML rendered in the browser with Figure 3b's CSS.

A quick note about using tabs with Wings applications: Using a tabbed UI with Wings is potentially a way to segregate various parts of a given record format, but a tabbed UI can't magically let a Wings user move from one RPG program to the next, and persist RPG state, by clicking tabs. Recall that Wings is driven by a procedural RPG program--tabs can't change that behavior.

Putting the CodePen code to work in an AVR ASP.NET Web app

The result of what we want in AVR from the CodePen code above is shown below in Figure 4a.

Figure 4a. AVR Web page with working tabs.

We have a few requirements that weren't present in the CodePen code:

  • a client-side event to fire when a tab is selected. If you're writing 100% server-side AVR, you don't really need this, but chances are that many will want to populate this UI with JavaScript and Ajax, on demand when a tab changes, . This event provides that possibility.
  • persist the selected tab across ASP.NET postbacks. When the user clicks a tab's child button, that tab needs to be the one to get focus when the postback returns the page back to the browser.
  • determine which tab had focus when a tab's child button was clicked. Again this may not be absolutely necessary but might be nice to have (and it's very easy to implement, so why not!).

In this example, default.aspx has a tabbed UI defined on it (the one shown in Figure 4a). Its markup is shown below in Figure 4b.

<%@ Page Language="AVR" AutoEventWireup="false" CodeFile="Default.aspx.vr" Inherits="_Default" %>

<!DOCTYPE html>
<html lang="en" >
<head runat="server">
    <title>Tab Demo</title>
    <link rel="stylesheet" href="assets/css/tabs.css" />
</head>
<body>
    <h3>Tabbed UI example with ASNA Visual RPG</h3>
    <form id="form1" runat="server">
    <div>
        <section class="tabs-container">
            <input id="tab-one" type="radio" name="grp" checked="checked" class="tab-check"/>
            <label for="tab-one">Tab One</label>
            <div class="tab-content">
                Bacon ipsum dolor amet beef turkey pork chop cupim. Beef ribs rump meatball t-bone ball 
                tip jowl chicken sirloin cupim doner corned beef ribeye. Burgdoggen tenderloin venison ball 
                tip. Brisket salami ball tip venison, swine landjaeger tenderloin ground round drumstick
                bresaola capicola sirloin pork chop boudin flank.
                <br />
                <br />
                <asp:Button ID="Button1" runat="server" Text="Cause ASP.NET PostBack" 
                    CommandArgument="Tab1" />
            </div>

            <input id="tab-two" type="radio" name="grp" class="tab-check"/>
            <label for="tab-two">Tab Two</label>
            <div class="tab-content">
                Chuck short ribs tongue, corned beef turducken sausage prosciutto porchetta pork turkey 
                venison jerky spare ribs. Pork loin leberkas filet mignon pork chop brisket ground
                round picanha porchetta venison swine jerky cupim. Ball tip tri-tip burgdoggen, andouille
                 pastrami ribeye landjaeger doner beef ribs capicola pig tongue tail ham. 
                <br />
                <br />
                <asp:Button ID="Button2" runat="server" Text="Cause ASP.NET PostBack"
                    CommandArgument="Tab2" />
            </div>

            <input id="tab-three" type="radio" name="grp" class="tab-check"/>
            <label for="tab-three">Tab Three</label>
            <div class="tab-content">
                Notice that with CSS Flex there isn't a fixed height for the tabbed area!
                <br />
                <br />
                <asp:Button ID="Button3" runat="server" Text="Cause ASP.NET PostBack"
                    CommandArgument="Tab3" />
            </div>
        </section>    
    </div>
    </form>

    <script>
        if (<%=(Not Page.IsPostBack).ToString().ToLower()%> ) {
            sessionStorage.setItem('currentTab', null);
        }
    </script>

    <script src="assets/js/tabs.js"></script>
</body>
</html>

Figure 4b. An ASPX page with markup for a tabbed UI.

Most of the HTML from the CodePen example was added directly to the ASPX markup above. However, note the tab content sections (the div tabs with tab-content classes) all have an ASP.NET button control. These tab content areas could have any ASP.NET controls in them. I included buttons to easily prove and test tab behavior across page postbacks.

You'll also note some slightly mysterious JavaScript at the near the bottom of the ASPX markup in Figure 4b. 

if (<%=(Not Page.IsPostBack).ToString().ToLower()%> ) {
    sessionStorage.setItem('currentTab', null);
}

This JavaScript uses the ASP.NET-provided client-side Page.IsPostBack property to determine if the page is not in PostBack state (just like you would check for NOT IsPostBack in the server-side AVR). It's likely that if a user moves from the default.aspx page to another page, you want the default tab focused when control returns to default.aspx. If so, leave this code in place. Otherwise, remove it, and when the user returns to the default.aspx page, the tab that had focus when the user left the page will once again be focused. (Thanks to JavaScript's sessionStorage object, which I'll discuss shortly.) In a product app, you'd probably add this code to the master page.

The CSS

The original CodePen CSS was a little too generic in that it didn't assume there could be other div tags or other markup on the page. If you compare the markup in Figure 4b to the CodePen's markup, you'll see that Figure 4b nests the tab's section area inside a div tag and added the CSS classes tab-container, tab-check, and tab-content. These classes enabled the CSS to be a little specific as to what parts of the markup it applied to.

The CSS to render Figure 4b's ASPX markup is shown below in Figure 4c. This is lifted nearly as-is from the CodePen code, with the exception that I added the three additional class names to make things more specific and modified the selectors accordingly. This tabbed UI code would only apply to tabs defined within a section tag marked with a tabs-container class. If you needed two sets of tabs on a page, you'll need to modify this code. You might want two sets of tabs if you need to nest tabs, but if you do that, you're veering into tab overload, especially for a mobile app.

body {
    font-family: Arial;
    color: #333;
}

/*
Select section tags with class=tabs-container
*/
section.tabs-container {
    display: -webkit-flex;
    display: flex;
    -webkit-flex-wrap: wrap;
    flex-wrap: wrap;
}

/*
Select label tags that is immediately preceded by an 
input with type=radio with class=tab-check and is a descendent of 
a section tag with class=tabs-container
*/
section.tabs-container input[type=radio].tab-check + label {
    background: #eee; 
    border: 1px solid #ddd; 
    padding: .7em 1em;
    cursor: pointer;
    z-index: 1;
    margin-left: -1px;
}

/*
Select the _first_ label tag that is immediately preceded by an 
input with type=radio with class=tab-check and is a descendent of 
a section tag with class=tabs-container
*/
section.tabs-container input[type=radio].tab-check + label:first-of-type {
    margin-left: 0;
}

/*
Select div tags with class=tab-content that is a descendant
of a section tab with class=tabs-container    
*/
section.tabs-container div.tab-content {
    width: 100%;
    margin-top: -1px;
    padding: 1em;
    border: 1px solid #ddd;
    -webkit-order: 1;
    order: 1;
}

/* 
Select div tags with class=tab-content that is a descendent of a section tag
with class=tabs-container or a checked input tag with type=radio and 
and class=tab-check and is a descendent of a section tag with 
class=tabs-container
*/       
section.tabs-container input[type=radio].tab-check, section.tabs-container div.tab-content {
    display: none;
}

/* 
Select label tags immediately preceded by a checked input tag with type=radio 
that is a descendant of a section tag with class=tabs-container  
*/       
section.tabs-container input[type=radio].tab-check:checked + label {
    background: #fff;
    border-bottom: 1px solid #fff;
}

/* 
Select div tags with class=tab-content that is immediately preceeded by a 
label immediately preceded by a checked input tag with type=radio that 
is a descendant of a section tag with class=tabs-container  
*/       
section.tabs-container input[type=radio].tab-check:checked + label + div.tab-content   {
    display: block;
}    

Figure 4c. CSS to render Figure 4b's ASPX markup as a tabbed UI.

I'm not going to step through the CSS to explain in detail how it works (this article has already taken on a life of its own). The CSS in Figure 4c has comments that explain each selector. Use these links to further study Figure 4c's CSS:

The markup and CSS from Figures 4b and 4c produce the UI shown in Figure 4a. No JavaScript is required for that presentation. However, we need a little JavaScript to manage tab state and provide an event handler when tabs are clicked. That JavaScript is shown below in Figure 4d.

// Section A.  
var tabInputs = document.querySelectorAll('input.tab-check');
for (i = 0; i < tabInputs.length; ++i) {
    tabInputs[i].addEventListener("click", function (e) {
        var currentTab = this;
        sessionStorage.setItem('currentTab', 'input#' + this.id);
    });
}

// Section B.
if (sessionStorage.getItem('currentTab') === null) {
    var defaultTabId = 'input#tab-one';
    document.querySelector(defaultTabId).click();
}
else {
    var defaultTabId = sessionStorage.getItem('currentTab');
    document.querySelector(defaultTabId).click();
}

Figure 4d. The JavaScript needed to manage tab state and click events.

Note that there isn't any need for jQuery with this JavaScript. jQuery resolved many issues back in the day, but today thanks to Web technology maturity (driven in no small part by jQuery's influence and example) it's good to forgo jQuery if you can.

The JavaScript above is broken into two sections:

  • Section A. This section collects all input tags decorated with a tab-check class. These are the radio boxes that the CSS uses to define each tab. Thanks to the clever CSS they won't ever show as radio buttons. An click event handler is added for each input tag in the resulting collection. This event handler stores the ID of the input tag clicked (the tab clicked) in JavaScript's sessionStorage object. The input ID stored in sessionStorage is used to ensure the selected tab persists across page postbacks. While not shown here, you would typically add any other tab logic to be performed here before the tab is displayed (as might especially be the case if you're using Ajax to populate tabs).

    Essentially, the sessionStorage object works like an in-memory cookie. You can add as many value pairs to it as necessary. sessionStorage is cleared when the browser or browser tab is closed. JavaScript also has a localStorage. It works just like the sessionStorage object except it persists across browser instances. sessionStorage and localStorage are widely supported across browsers today.

    Once sessionStorage value is set, it persists for the entire browser session. That is, if you set a value in sessionStorage on Page 1, it is available to Page 2. Therefore, if a user moves off of your tabbed page, this code would position them on the previously selected tab when they return to the tabbed page. However, those two lines of JavaScript we discussed earlier on the ASPX page itself, clears the currentTab key from sessionStorage so the default tab is displayed when the user returns to the tabbed page. As previously mentioned, you can remove that code so that the user is returned to the previously-selected tab if that behavior is desired.

  • Section B. This section is essentially performing "page load" responsibilities. It governs what tab is displayed when the page is rendered. If the currentTab key value hasn't been set then the default tab is identified (which is up to you to identify) and that tab is virtually clicked (letting the click event do any normal tag content loading and tab id storage.

    If the currentTab key has a value, that tab is virtually clicked. This gives it focus and performs whatever logic is associated with its click event.

Server side code

There is one more minor consideration for this basic example. As shown in Figure 4a, each tab has an ASP.NET button on it. It's easy to determine an ASP.NET button's parent tab by setting its CommandArgument property to a value that defines its parent tab (any control that causes a postback has this property). For example, when a button's CommandArgument is set to Tab1, you can use the AVR server-side code to read that button's CommandArgument and base tab-specific processing on its value.

BegSr Button2_Click Access(*Private) Event(*This.Button2.Click)
    DclSrParm sender Type(*Object)
    DclSrParm e Type(System.EventArgs)

    DclFld ParentTab Type(*String) 

    ParentTab = (Sender *AS Button).CommandArgument
EndSr

Figure 5. AVR server-side code to read a button's CommandArgument property.

Over and out

There may be other tabbed UI features you need that aren't provided here, but this technique lays solid groundwork that you can build on for tabs populated from either the server side or the client side. The markup, CSS, and JavaScript all wrapped up in an AVR ASP.NET example downloaded by using the "download" button at the top of this page.