Bootstrap: Control multiple tab panels with single tab nav
Asked Answered
C

3

8

I want to control two differnt tab contents with a single tab navigation.

In Bootstra 3 I had a solution with comma separated data targets (like in this example: https://mcmap.net/q/594687/-control-multiple-tab-contents-with-one-nav-tabs).

But in Bootsrap 4, this way is not working anymore for tabs.

The collapse component could work with multiple targets, but I couldn't use it for the tabs: https://getbootstrap.com/docs/4.0/components/collapse/#multiple-targets

Is there an other way to do this?

Here's my code:

<div class="B">
    <div class="container">

        <div class="tab-content" id="ueberTabA">
            <div class="tab-pane fade" id="panel_a_first" role="tabpanel" aria-labelledby="first-tab">
                A First 
            </div>
            <div class="tab-pane fade show active" id="panel_a_second" role="tabpanel" aria-labelledby="second-tab">
                A Second
            </div>
            <div class="tab-pane fade" id="panel_a_third" role="tabpanel" aria-labelledby="third-tab">
                A Third
            </div>
        </div>
    </div>
</div>

<div class="container">
    <ul class="nav nav-tabs" id="ueberTab" role="tablist">
        <li class="nav-item"><a class="nav-link" id="first-tab" data-target="#panel_b_first, #panel_a_first" data-toggle="tab" href="#first" role="tab" aria-controls="first" aria-selected="false">first</a></li>
        <li class="nav-item"><a class="nav-link active" id="second-tab" data-target="#panel_b_second, #panel_a_second" data-toggle="tab" href="#second" role="tab" aria-controls="second" aria-selected="true">second</a></li>
        <li class="nav-item"><a class="nav-link" id="third-tab" data-target="#panel_b_thrid, #panel_a_third" data-toggle="tab" href="#third" role="tab" aria-controls="third" aria-selected="false">Unser third</a></li>
    </ul>
    <div class="tab-content" id="ueberTabB">
        <div class="tab-pane fade" id="panel_b_first" role="tabpanel" aria-labelledby="first-tab">
            B First 
        </div>
        <div class="tab-pane fade show active" id="panel_b_second" role="tabpanel" aria-labelledby="second-tab">
            B Second
        </div>
        <div class="tab-pane fade" id="panel_b_thrid" role="tabpanel" aria-labelledby="third-tab">
            B Third
        </div>
    </div>
</div>
Cyclothymia answered 22/1, 2018 at 16:39 Comment(0)
D
6

Bootstrap 5 (update 2021)

This is not possible with Tabs "out of the box". However, JS could be used to show secondary tab-panes...


document.querySelectorAll('button[data-bs-toggle="tab"]').forEach((t,i)=>{
    t.addEventListener('show.bs.tab', function (e) {
        let targetClass = t.dataset.bsTarget
        var pane = document.querySelector('#secondTabContent '+targetClass)
        var sibling = document.querySelector('#secondTabContent .tab-pane.active')
        // hide 2nd pane sibling
        sibling.classList.remove('show')
        sibling.classList.remove('active')
        // show 2nd pane
        pane.classList.add('show')
        pane.classList.add('active')
    })  
})

Bootstrap 5 - multiple Tab panes from single Nav


Bootstrap 4 (original answer)

This is not possible in Bootstrap 4.0.0. This is currently an open issue, and a possible "idea" for Bootstrap 4.1.

https://github.com/twbs/bootstrap/issues/19964

Disjuncture answered 8/2, 2018 at 8:58 Comment(2)
I'm having a hard time being able to tell from the convo there whether this has actually been done yet. Hoping it's possible for BS5?Fatal
With Bootstrap 5 it's only possible using JavaScript: codeply.com/p/ey1cO4bL5yDisjuncture
H
3

I've actually found a way to make this work. It's not the most beautiful solution but it does make the job.

Here is what you'll need to do:

$(document).on('click', '#ueberTab a', function(e) {
          otherTabs = $(this).attr('data-secondary').split(',');
          for(i= 0; i<otherTabs.length;i++) {
            nav = $('<ul class="nav d-none" id="tmpNav"></ul>');
            nav.append('<li class="nav-item"><a href="#" data-toggle="tab" data-target="' + otherTabs[i] + '">nav</a></li>"');
            nav.find('a').tab('show');
          }
        });
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script><div class="B">
    <div class="container">

        <div class="tab-content" id="ueberTabA">
            <div class="tab-pane fade" id="panel_a_first" role="tabpanel" aria-labelledby="first-tab">
                A First 
            </div>
            <div class="tab-pane fade show active" id="panel_a_second" role="tabpanel" aria-labelledby="second-tab">
                A Second
            </div>
            <div class="tab-pane fade" id="panel_a_third" role="tabpanel" aria-labelledby="third-tab">
                A Third
            </div>
        </div>
    </div>
</div>

<div class="container">
    <ul class="nav nav-tabs" id="ueberTab" role="tablist">
        <li class="nav-item"><a class="nav-link" id="first-tab" data-target="#panel_b_first" data-secondary="#panel_a_first" data-toggle="tab" href="#first" role="tab" aria-controls="first" aria-selected="false">first</a></li>
        <li class="nav-item"><a class="nav-link active" id="second-tab" data-target="#panel_b_second" data-secondary="#panel_a_second" data-toggle="tab" href="#second" role="tab" aria-controls="second" aria-selected="true">second</a></li>
        <li class="nav-item"><a class="nav-link" id="third-tab" data-target="#panel_b_thrid" data-secondary="#panel_a_third" data-toggle="tab" href="#third" role="tab" aria-controls="third" aria-selected="false">Unser third</a></li>
    </ul>
    <div class="tab-content" id="ueberTabB">
        <div class="tab-pane fade" id="panel_b_first" role="tabpanel" aria-labelledby="first-tab">
            B First 
        </div>
        <div class="tab-pane fade show active" id="panel_b_second" role="tabpanel" aria-labelledby="second-tab">
            B Second
        </div>
        <div class="tab-pane fade" id="panel_b_thrid" role="tabpanel" aria-labelledby="third-tab">
            B Third
        </div>
    </div>
</div>
Highly answered 25/11, 2018 at 22:53 Comment(0)
S
0

I guess it's worth to widen a bit Carol Skelly's answer to more than 2 target tab panels and keep built-in hide/show animation working. BS 5 exposes only fade animation so let's write a proof of concept for it.

Besides, BS 5 provides a showcase with nav links, that you may get rid of if you need more complex layouts.

So here is a proof of concept with minimal markup :

/**
 * Allow a bootstrap single nav tab to target multiple tab panels
 */
document.querySelectorAll( '[data-bs-toggle="tab"][data-bs-target]' ).forEach( ( navTab ) => {
  const targetPanes = document.querySelectorAll( navTab.dataset.bsTarget )

  if( targetPanes.length > 1 ) {
    navTab.addEventListener( 'show.bs.tab', ( e ) => {
      const activeNav     = navTab.closest( '[role="tablist"]' ).querySelector( '.active[role="tab"]' )
      const activePanes   = document.querySelectorAll( activeNav.dataset.bsTarget )

      // Hide other panes
      activePanes.forEach( ( activePane ) => {
        // Ensure selector targets only tab panels
        if( activePane.matches( '[role="tabpanel"]' ) ) {
          const transProperty     = activePane.classList.contains( 'fade' ) ? 'opacity' : null
          const outTransDuration  = transProperty ? utils.toMilliseconds( utils.getComputedTransitions( activePane, '', transProperty ).duration ) : 0

          // Remove class making pane visible
          activePane.classList.remove( 'show' )
          // Wait for transition...
          setTimeout( () => {
            // Then hide pane
            activePane.classList.remove( 'active' )

            targetPanes.forEach( ( targetPane ) => {
              // Ensure selector targets only tab panels
              if( targetPane.matches( '[role="tabpanel"]' ) ) {
                const transProperty   = targetPane.classList.contains( 'fade' ) ? 'opacity' : null
                const inTransDuration = transProperty ? utils.toMilliseconds( utils.getComputedTransitions( targetPane, '', transProperty ).duration ) : 0
                
                // Show pane
                targetPane.classList.add( 'active' )
                // Can't figure why, but this ensures animation of target panes beyond first lasts as long as animation of first
                setTimeout( () => {
                  targetPane.classList.add( 'show' )
                }, 0 )
              }
            } )
          }, outTransDuration )
        }
      } )

    } )
  }
} )

/**
 * Utils to get transition duration of a CSS property
 */
const utils = {
  getComputedTransitions: ( elem, pseudo, property ) => {

    const computedStyles  = window.getComputedStyle( elem, pseudo )
    const delays          = computedStyles.getPropertyValue( 'transition-delay' ).split( ', ' )
    const durations       = computedStyles.getPropertyValue( 'transition-duration' ).split( ', ' )
    const properties      = computedStyles.getPropertyValue( 'transition-property' ).split( ', ' )
    const timingFunctions = computedStyles.getPropertyValue( 'transition-timing-function' ).split( ', ' )
  
    let transitions = {}
  
    for( let i in properties ) {
      transitions[properties[i] || 'all'] = {
        duration: durations[i],
        delay: delays[i],
        timingFunction: timingFunctions[i],
      }
    }
  
    if( property ) {
      return transitions[property] ? transitions[property] : transitions['all']
    }
  
    return transitions
  
  },

  toMilliseconds: ( string ) => {

    if( string.match( /^[0-9.]+s$/g ) ) {
      return parseFloat( string ) * 1000
    }
    if( string.match( /^[0-9.]+ms$/g ) ) {
      return parseFloat( string )
    }
  
    return string
  
  }
    
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"/>
<div role="tablist">
  <button type="button" class="active" data-bs-toggle="tab" data-bs-target=".pane-first" role="tab" aria-controls="pane-first" aria-selected="true">Tab first</button>
  <button type="button" data-bs-toggle="tab" data-bs-target=".pane-second" role="tab" aria-controls="pane-first" aria-selected="false">Tab second</button>
</div>
...
<div class="tab-content">
  <div class="tab-pane fade pane-first active show" role="tabpanel">Block 1, pane first</div>
  <div class="tab-pane fade pane-second" role="tabpanel">Block 1, pane second</div>
</div>
...
<div class="tab-content">
  <div class="tab-pane fade pane-first active show" role="tabpanel">Block 2, pane first</div>
  <div class="tab-pane fade pane-second" role="tabpanel">Block 2, pane second</div>
</div>

Fade transitions are not really synchronous. I guess native BS feature interferes when block 1 tabpanels toggle.

Sakhuja answered 10/4 at 15:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.