New - unofficial - feature: TABS - Hide/Show multiple blocks horizontally

Hi all! Great news!
I managed to create an unofficial (Strictly done by myself: not the responsibility of Softr) tabs feature for Softr. A big thanks to Artur who helped me (at late night😂) with completing and improving my javascript snippet!

For those who don’t know what are tabs, and everybody else, Let’s not spoil it more than it needs. The demo is here: https://test-play.softr.app/hide-show-tabs?recordId=recZV04ZNE3pvMnKM

Everything is made with a CTA block with 3 buttons (you can have as many as you want), a JS snippet and the blocks I wanted to display. Nothing more.

Explanation

1) The Tabs
Made of one CTA block + one custom javascript inside a custom code block. Nothing more.
You can add or retrieve as many buttons as you want in your CTA block, you will just need to adapt the code accordingly.

To display blocks inside tabs, just add them vertically in the Softr studio. Don’t worry, they won’t appear one below another in live environment. As an example, here is my setup in the Softr studio (see screenshot at the end of the post).

For a better UX, don’t forget to add an action to all of your buttons. This action should be a “scroll to section”. It should scroll to the same CTA block where the buttons are.

The JS snippet is the following one, to be inserted in a custom code block (reduce the padding top and the padding bottom of this block to the almost minimum to make it disappear, visually, from your page)

<script>
    // hide blocks 
    window.addEventListener('DOMContentLoaded', (event) => {
        document.getElementById("table1").style.display = "block";
        document.getElementById("form1").style.display = "none";
        document.getElementById("chart1").style.display = "none"
    });


    // This is being called when CTA block is rendered
    window.addEventListener('block-loaded-cta3', () => {
        console.log('CTA Block loaded');

        // function for button click handler
        const buttonClickHandler = (e) => {
            if (e.target.closest('#cta3 a[data-element="button"]:nth-child(1)')) {
                console.log('Click Button 1');
                document.getElementById("table1").style.display = "block";
                document.getElementById("form1").style.display = "none";
                document.getElementById("chart1").style.display = "none";
            }
            if (e.target.closest('#cta3 a[data-element="button"]:nth-child(2)')) {
                console.log('Click Button 2');
                document.getElementById("form1").style.display = "block";
                document.getElementById("table1").style.display = "none";
                document.getElementById("chart1").style.display = "none";
            }
            if (e.target.closest('#cta3 a[data-element="button"]:nth-child(3)')) {
                console.log('Click Button 3');
                document.getElementById("chart1").style.display = "block";
                document.getElementById("form1").style.display = "none";
                document.getElementById("table1").style.display = "none";
            }
        };
        
        //
        document.body.addEventListener('click', buttonClickHandler);
    });

</script>

Here, The first tab is not hidden on load, as it is quite classic from a UX point that the first tab should be visible. Of course if you want everything to be hidden, it’s possible by writing document.getElementById(“table1”).style.display = “none” instead of “block”.
The result will be this: https://test-play.softr.app/tab-features-only

Note that you can also use a JS snippet using Jquery. There will be a slight difference: you will be able to hide and show every block by clicking and re-clicking on the corresponding button + a nice slide effect.
The result will be this: https://test-play.softr.app/jquery-tabs-feature

<script>
    // hide blocks 
    window.addEventListener('DOMContentLoaded', (event) => {
        document.getElementById("table2").style.display = "none";
        document.getElementById("form2").style.display = "none";
        document.getElementById("chart2").style.display = "none";
    });


    // This is being called when CTA block is rendered
    window.addEventListener('block-loaded-cta1', () => {
        console.log('CTA Block loaded');

        // function for button click handler
        const buttonClickHandler = (e) => {
            if (e.target.closest('#cta1 a[data-element="button"]:nth-child(1)')) {
                console.log('Click Button 1');
                $("#table2").toggle("slow");
               if ($("#form2:visible").lenght);
                 $("#form2").hide();
               if ($("#chart2:visible").lenght);
                 $("#chart2").hide();
            }
            if (e.target.closest('#cta1 a[data-element="button"]:nth-child(2)')) {
                console.log('Click Button 2');
                $("#form2").toggle("slow");
               if ($("#table2:visible").lenght);
                 $("#table2").hide();
               if ($("#chart2:visible").lenght);
                 $("#chart2").hide();
            }
            if (e.target.closest('#cta1 a[data-element="button"]:nth-child(3)')) {
                console.log('Click Button 3');
                $("#chart2").toggle("slow");
               if ($("#form2:visible").lenght);
                 $("#form2").hide();
               if ($("#table2:visible").lenght);
                 $("#table2").hide();
            }
        };
        
        //
        document.body.addEventListener('click', buttonClickHandler);
    });

</script>

2) “One page” apps enabled
The page with some fake projects (https://test-play.softr.app/hide-show-tabs?recordId=recZV04ZNE3pvMnKM) should take your attention as the list block and the list details block are inverted.
And everytime you reload the page, the same recordId in the Url parameter appears. This solution was found by David Coletta.
Here is the setup: add a list block of your choice. Below, add a list details block of your choice. In the custom code header of the page add this code:

<script>
if (!window.location.href.includes("/r/rec")) {
    const urlParams = new URLSearchParams(window.location.search);
    const recordId = urlParams.get('recordId');
    if (!recordId) {
        urlParams.set('recordId', "recZV04ZNE3pvMnKM")
        window.history.replaceState({}, '', `${location.pathname}?${urlParams}`);
    }
}
</script>

You will need to change “recZV04ZNE3pvMnKM” by the recordId you want to be shown each time a page is reloaded. That way, even if the user would land on the page without any recordId… The page would have one!
To find the ID of a record: go to your corresponding table in Airtable and add a formula field. Enter RECORDID() and that’s it, all the records will have their ID visible.

Another thing: you can see that the blocks displayed in the Tabs have different data according to the project you select. Pretty easy to do: you just have to use the right conditional filters, linked to the list details block! Do just like the tabs feature didn’t exist, the Softr pattern still works!

At the end, you will have a full project management with one page in Softr (add the page for data details in a modal, still a one page app from a final user perspective)

3) Leverage button clicks on your Softr app
Beyond giving you the ability to create a tabs feature, now you know how you can handle buttons of a CTA in Softr. So, be imaginative, I’m sure some other use cases can be found!

4) Be imaginative X Javascript = huge possibilities
Talking about being imaginative, all this shows you that even if Softr can be limited some times, some “not so big” workarounds can be found to improve it greatly.
Softr will have more and more long awaited features in the next weeks. Though, I strongly believe that our use of Softr will become more and more advanced. Javascript will always be needed
Oh! and of course if you want to use custom buttons inserted in an other custom code block, do it! The design can be improved, sure, but I wanted an easy solution made with native Softr components.
You can find custom buttons on shoelace.style or codepen.io or any other components library.

5) The story behind (if you care)
The workflow and the first draft of the JS snippet was made by myself since two weeks. The only thing that I couldn’t find is how to handle a Softr CTA button. Just this, just pointing a button, it drove me crazy.
David Coletta tried to help me with a solution I couldn’t understand (I am not a developer and I am learning javascript at a snail’s pace, so, cheers to David too and sorry for the disappointment that I am :sweat_smile:. Though if you want to insert it in Scissors, feel free!).
I tried a dozen of dev forums, couldn’t find a solution. Even if I’m super stubborn I had to cut it.
So I reached out to Artur during the day. He came back at almost midnight with a solution. “Dedication“ (Does he sleep? Not sure :thinking:). Cheers to @artur !

If something unclear, just ask.

6 Likes

In love with the implementation!

Great job.

Thanks for sharing.

2 Likes

@matthieu_chateau fantastic workaround. The inability to organise pages into tabs was one major drawback compared with Stacker, but with this I can make some far more sensibly arranged pages.

Well done and thank you for sharing! Hopefully this gets built-in to Softr soon :wink:

2 Likes

This :clap: is :clap: awesome :clap: and :clap: genius!

1 Like

Wow. I haven’t even read this whole thread yet — just played with the demo — but wanted to quickly say Thank You, @matthieu_chateau. This is absolutely great and I have a couple of perfect use case that I’ve been trying to get my head around where I think this tab feature will be the perfect solution…

2 Likes

Super impressive - thanks of the share!

2 Likes

Fantastic again @matthieu_chateau. Great work!

2 Likes

This so fantastic! Already using this everywhere on our site. It would be great to get code to change the text and background colors of the button that is clicked so that the user can tell which item they are looking at.

1 Like

Glad you love it!
The option to change the background colour of the clicked button : will try to do it tomorrow or after tomorrow. Changing the color or the text should be implemented too

Here it is : Note that it doesn’t use a CTA block from Softr but a hardcoded button group
Demo: https://test-play.softr.app/tabs-with-custom-buttons

<!DOCTYPE html>
<html>
<head>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');
         .button-group {
            display: flex;
            justify-content: center;
        }

        .button {
            background-color: #182939;
            color: white;
            font-family: 'Inter', sans-serif;
            font-size: 16px; 
            padding: 10px 20px; 
            margin-right: 30px;
            padding-top: 9px;
            padding-bottom: 9px;
            border-radius: 14px;
            border: none;
            cursor: pointer;
        }

        .button:active, .active {
            background-color: #f5e507;
            color: #182939;
            outline: none;
        }
    </style>
</head>
<body>
    <div class="button-group">
        <button class="button active" onclick="runButton1Script()">Button 1</button>
        <button class="button" onclick="runButton2Script()">Button 2</button>
        <button class="button" onclick="runButton3Script()">Button 3</button>
    </div>
     <script>
    const buttons = document.querySelectorAll('.button');
        buttons.forEach(button => {
        button.addEventListener('click', function() {
            buttons.forEach(b => b.classList.remove('active'));
            this.classList.add('active');
        });
    });
</script>
</body>
<script>
    // hide blocks 
    window.addEventListener('DOMContentLoaded', (event) => {
        document.getElementById("list1").style.display = "block";
        document.getElementById("list2").style.display = "none";
        document.getElementById("list3").style.display = "none"
    });

    function runButton1Script() {
        document.getElementById("list1").style.display = "block";
        document.getElementById("list2").style.display = "none";
        document.getElementById("list3").style.display = "none";
        document.getElementById("list1").scrollIntoView("list1");
        console.log("Button 1 clicked"); 
    }

         function runButton2Script() {
        document.getElementById("list2").style.display = "block";
        document.getElementById("list1").style.display = "none";
        document.getElementById("list3").style.display = "none";
        document.getElementById("list2").scrollIntoView("list2");
        console.log("Button 2 clicked");
    }
          function runButton3Script() {
        document.getElementById("list3").style.display = "block";
        document.getElementById("list1").style.display = "none";
        document.getElementById("list2").style.display = "none";
        document.getElementById("list3").scrollIntoView("list3");
        console.log("Button 3 clicked");
    }

</script>
</html>
1 Like

This is fantastic! Thanks so much!

1 Like

Update: I managed to make it work with the var iterator = document.evaluate() method suggested by David Coletta :tada:
It doesn’t change anything visually but this is another method to make your JS snippets work when they’re related to multiple Softr buttons.

Demo and full code snippet here: https://test-play.softr.app/tabs-for-softr-with-var-iterator-js-snippet

This is really excellent! Thank you @matthieu_chateau and @artur

I don’t really know much JS, but I have changed the script to work with my blocks and buttons, also I have made it show/hide multiple blocks in each tab per button click. It was easy enough to figure out after looking at the code you shared.

Simple yet powerful. This is exactly how I wanted it to be designed: no big setup (one JS + Softr blocks you use all the time, after all) and easy to modify. Thanks for your support!

1 Like

Hey @matthieu_chateau,

By the way, I shared this workaround today with a Softr user :slight_smile: He was so excited :slight_smile:

Thanks once again :star_struck:

Thanks @matthieu_chateau , great job :ok_hand:t2::slightly_smiling_face: