├── TapDancers.jpg ├── README.md ├── demo.html ├── jquery.tabdancer.css └── jquery.tabdancer.js /TapDancers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmkenz/TabDancer/HEAD/TapDancers.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TabDancer 2 | A Responsive Tabs Solution for jQuery 3 | 4 | Too many tabs to fit on one line? Hide the ones that don't fit, and reveal them on command. Shrink your viewport so that the tabs no longer fit on one line. A new 'more' tab will replace the hidden tabs. 5 | 6 | Demo: http://jamesmckenzie.ca/TabDancer 7 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TabDancer Demo 6 | 7 | 8 | 9 | 10 | 11 | 24 | 25 | 26 |

TabDancer jQuery Plugin

27 |

Too many tabs to fit on one line? Hide the ones that don't fit, and reveal them on command.

28 |

To see the magic, shrink your viewport so that the tabs no longer fit on one line. A new 'more' tab will replace the hidden tabs.

29 |
30 | 38 |
39 |
40 | Tab content 1 41 |
42 |
43 | Tab content 2 44 |
45 |
46 | Tab content 3 47 |
48 |
49 | Tab content 4 50 |
51 |
52 | Tab content 5 53 |
54 |
55 | Tab content 6 56 |
57 | 58 | 59 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /jquery.tabdancer.css: -------------------------------------------------------------------------------- 1 | .js-tabs-container { 2 | position: relative; 3 | } 4 | 5 | .js-tab-content { 6 | display: none; 7 | } 8 | 9 | .no-js .js-tab-content { 10 | display: block; 11 | } 12 | 13 | .tabs { 14 | margin: 0; 15 | padding: 0; 16 | list-style: none; 17 | display: block; 18 | overflow: hidden; 19 | position: relative; 20 | } 21 | 22 | .tabs:before, 23 | .tabs:after { 24 | content: ""; 25 | display: table; 26 | } 27 | 28 | .tabs:after { 29 | clear: both; 30 | zoom: 1; 31 | } 32 | 33 | .tabs li { 34 | float: left; 35 | margin-left: 5px; 36 | } 37 | 38 | .tabs li:first-child { 39 | margin-left: 0; 40 | } 41 | 42 | .tabs li, 43 | .tabs li a, 44 | .js-tab-stack li.tab-available { 45 | display: block; 46 | } 47 | 48 | .tabs li a { 49 | padding: 0.55em 1.1em; 50 | text-decoration: none; 51 | border-radius: 4px; 52 | color: #337AB7; 53 | } 54 | 55 | .tabs li a:hover { 56 | background: #f5f5f5; 57 | } 58 | 59 | .tabs .tab-active a, 60 | .tabs .tab-active a:hover { 61 | cursor: default; 62 | color: #fff; 63 | background: #337AB7; 64 | } 65 | 66 | .js-tab-content-container:focus { 67 | outline: none; 68 | } 69 | 70 | li.js-tab-toggler { 71 | display: none; 72 | float: right; 73 | } 74 | 75 | .js-tab-stack li { 76 | display: none; 77 | } 78 | 79 | .js-tab-stack li.tab-available, 80 | .js-tab-stack li.tab-active, 81 | .js-tab-stack-open li, 82 | .js-tab-stack li.js-tab-toggler { 83 | display: block; 84 | } 85 | 86 | .js-tab-toggler a:after { 87 | content: " +"; 88 | } 89 | 90 | .js-tab-stack-open li.js-tab-clone, 91 | .js-tab-stack-open-complete li { 92 | clear: left; 93 | } 94 | 95 | .js-tab-clone-container { 96 | position: absolute; 97 | top: 0; 98 | left: 0; 99 | visibility: hidden; 100 | } 101 | 102 | .js-tab-stack-open li.js-tab-clone, 103 | .js-tab-stack-open li.js-ready-for-anim, 104 | .js-tab-stack-open-complete li { 105 | margin-left: 0; 106 | } 107 | 108 | li.js-ready-for-anim { 109 | position: absolute; 110 | transition: all 500ms cubic-bezier(0.130, 0.965, 0.380, 0.985); 111 | } 112 | 113 | li.js-ready-for-anim a:hover { 114 | background: none; 115 | } 116 | 117 | .js-tab-stack-open li.tab-active a:hover { 118 | background: #337AB7; 119 | } 120 | 121 | .js-tab-stack-open li.js-tab-toggler { 122 | display: none; 123 | } -------------------------------------------------------------------------------- /jquery.tabdancer.js: -------------------------------------------------------------------------------- 1 | /*Tab Dancer jQuery plugin 2 | // Author: James McKenzie 3 | // jamesmckenzie.ca 4 | // Version: 0.8; 5 | */ 6 | 7 | $.fn.TabDancer = function(){ 8 | 9 | var $tabsContainer = $(this), 10 | $tabs = $tabsContainer.find('li'), 11 | $activeTab = $tabs.find('.tab-active').first(), 12 | $tabToggler = $('
  • '), 13 | $tabTogglerLink = $tabToggler.find('a').first(), 14 | textMore = $tabsContainer.data('more-tabs-text') || "More"; 15 | 16 | //Add accessibility attributes and show active content 17 | $tabsContainer.attr('role', 'tablist'); 18 | $tabs.each(function(){ 19 | var $tab = $(this), 20 | targetID = $tab.children('a').attr('href').replace('#', ''); 21 | 22 | if($tab.is('.tab-active')){ 23 | $('#'+targetID).show(); 24 | } 25 | $tab.attr('role', 'tab'); 26 | $tab.attr('aria-controls', targetID); 27 | }); 28 | 29 | //Add tab toggler element 30 | $tabTogglerLink.append(''+textMore+''); 31 | $tabs.last().after($tabToggler); 32 | 33 | //Responsive tab toggler to reveal hidden tabs 34 | var toggleTabs = function(event){ 35 | event.preventDefault(); 36 | 37 | //expand the tabs 38 | if(!$tabsContainer.hasClass('js-tab-stack-open')){ 39 | //Allow auto height on the tabs container 40 | $tabsContainer.css('height', 'auto'); 41 | 42 | $tabsContainer.toggleClass('js-tab-stack').toggleClass('js-tab-stack-open'); 43 | 44 | //ANIMATE TRANSITION 45 | var $tabClonesContainer = $('
    '); 46 | $tabsContainer.append($tabClonesContainer); 47 | //Get target y-axis positions of each item 48 | for(var i = 0; i < $tabs.length; i++){ 49 | 50 | $tabs[i].tabClone = $($tabs[i]).clone(); 51 | $tabs[i].tabClone.addClass('js-tab-clone'); 52 | $tabClonesContainer.append($tabs[i].tabClone); 53 | 54 | } 55 | 56 | //Create vertical space during animation 57 | $tabsContainer.height($tabClonesContainer.height()); 58 | 59 | var $tabClones = []; 60 | 61 | //Before we measure the left offset of each item, ensure they all fit on one line temporarily 62 | $tabsContainer.css('width', '9999px'); 63 | 64 | for(var i = $tabs.length-1; i > 0; i = i-1){ 65 | $tabs[i].targetTop = $tabs[i].tabClone[0].offsetTop; 66 | $tabs[i].sourceLeft = $tabs[i].offsetLeft - parseInt($($tabs[i]).css('margin-left')); 67 | 68 | if(i==1){ 69 | //Return the tabs container width to normal 70 | $tabsContainer.css('width',''); 71 | } 72 | 73 | 74 | $($tabs[i]).css({'top':'0', 'left':$tabs[i].sourceLeft}); 75 | $($tabs[i]).addClass('js-ready-for-anim'); 76 | 77 | (function(x){ 78 | setTimeout(function(){ 79 | $($tabs[x]).css({'top':$tabs[x].targetTop, 'left':'0'}); 80 | }, 1); /*This tiny delay allows the CSS animation to register a change*/ 81 | })(i); 82 | 83 | //If we are complete all our animations 84 | if(i == 1) { 85 | setTimeout(function(){ 86 | $tabsContainer.addClass('js-tab-stack-open-complete'); 87 | $('.js-ready-for-anim').each(function(){ 88 | 89 | $(this).css({ 90 | 'top':'', 91 | 'left':'' 92 | }) 93 | .removeClass('js-ready-for-anim'); 94 | }); 95 | $tabClonesContainer.remove(); 96 | 97 | //Return the tabs container height to normal 98 | $tabsContainer.css('height',''); 99 | 100 | }, 500); /*This delay must match the animation length*/ 101 | } 102 | } 103 | 104 | //Move keyboard focus to active tab 105 | $(this).siblings('.tab-active').find('a').focus(); 106 | } else { 107 | //Collapse the tabs 108 | 109 | for(var i = $tabs.length-1; i > 0; i = i-1){ 110 | $tabs[i].sourceTop = $tabs[i].offsetTop; 111 | 112 | $($tabs[i]).css({'top':$tabs[i].sourceTop, 'left':'0'}); 113 | $($tabs[i]).addClass('js-ready-for-anim'); 114 | } 115 | 116 | $tabsContainer.removeClass('js-tab-stack-open-complete') 117 | .addClass('js-tab-stack') 118 | .removeClass('js-tab-stack-open'); 119 | 120 | checkTabsWidth(); 121 | 122 | //Set the active tab's 'left' property to be right after the last 'available' tab 123 | var rightMostLocation = $tabs[$tabs.length - 1].sourceLeft; 124 | 125 | for(var i = 1; i < $tabs.length; i = i+1){ 126 | if(!$($tabs[i]).hasClass('tab-available') && $($tabs[i-1]).hasClass('tab-available')){ 127 | rightMostLocation = $tabs[i].sourceLeft; 128 | } 129 | 130 | if($($tabs[i]).hasClass('tab-active') && !$($tabs[i-1]).hasClass('tab-available') ){ 131 | $tabs[i].sourceLeft = rightMostLocation; 132 | } 133 | } 134 | 135 | 136 | for(var i = $tabs.length-1; i > 0; i = i-1){ 137 | 138 | (function(x){ 139 | setTimeout(function(){ 140 | $($tabs[x]).css('top', '0'); 141 | $($tabs[x]).css('left', $tabs[x].sourceLeft); 142 | }, 1); 143 | })(i); 144 | 145 | 146 | 147 | if(i == 1) { 148 | setTimeout(function(){ 149 | $('.js-ready-for-anim').each(function(){ 150 | $(this).css({ 151 | 'top':'', 152 | 'left':'' 153 | }); 154 | }); 155 | $('.js-ready-for-anim').removeClass('js-ready-for-anim'); 156 | }, 500); 157 | } 158 | } 159 | 160 | } 161 | }; 162 | 163 | //Check width of tabs to see if they all fit in viewport 164 | var checkTabsWidth = function(){ 165 | 166 | if(!$tabsContainer.hasClass('js-tab-stack-open')) { 167 | 168 | $tabsContainer.removeClass('js-tab-stack'); 169 | 170 | var listWidth = $tabsContainer.width() - 10, 171 | itemWidth = 0, 172 | eachItemWidth = new Array(), 173 | eachItemObject = new Array(), 174 | marginAmount, 175 | activeItemWidth = $tabsContainer.children('.tab-active').width(), 176 | i = 0; 177 | 178 | //Set fixed height to tabs container to hide any items that overflow to a new line 179 | var $tabsContainerClone = $tabsContainer.clone(); 180 | $tabsContainerClone.find('li').not(':first-child').remove(); 181 | $tabsContainerClone.css('visibility', 'hidden'); 182 | $tabsContainer.after($tabsContainerClone); 183 | $tabsContainer.css('height', $tabsContainerClone.height()); 184 | $tabsContainerClone.remove(); 185 | 186 | $tabsContainer.children('li').not('.js-tab-toggler').each(function() { 187 | $el = $(this); 188 | $el.removeClass('tab-available'); 189 | marginAmount = parseInt($el.css('margin-left')) + parseInt($el.css('margin-right')); 190 | eachItemWidth[i] = $el.outerWidth() + marginAmount; 191 | eachItemObject[i] = $el; 192 | itemWidth = itemWidth + eachItemWidth[i]; 193 | i ++; 194 | }); 195 | 196 | if(itemWidth > listWidth) { 197 | $tabsContainer.addClass('js-tab-stack').find('.tabs-more').show(); 198 | 199 | //Check again to be sure they still fit. If not, hide the .tabs-more text 200 | itemWidth = 0; 201 | 202 | $tabsContainer.children('.tab-active, .js-tab-toggler').each(function() { 203 | $el = $(this); 204 | marginAmount = parseInt($el.css('margin-left')) + parseInt($el.css('margin-right')); 205 | itemWidth = itemWidth + $el.outerWidth() + marginAmount; 206 | 207 | if($el.hasClass('tab-active')){ 208 | $el.addClass('tab-available'); 209 | } 210 | }); 211 | 212 | if(itemWidth > listWidth) { 213 | $tabsContainer.find('.tabs-more').hide(); 214 | } 215 | 216 | //See if any other tabs can fit in (set to 'tab-available') 217 | var tabTogglerWidth = $tabsContainer.children('.js-tab-toggler').width(), 218 | remainingWidth = listWidth - activeItemWidth - tabTogglerWidth; 219 | 220 | for (var x = 0; x < eachItemObject.length; x++) { 221 | 222 | if(!eachItemObject[x].hasClass('tab-active')) { 223 | remainingWidth = remainingWidth - eachItemWidth[x]; 224 | 225 | if (remainingWidth > 0) { 226 | eachItemObject[x].addClass('tab-available'); 227 | } 228 | } 229 | 230 | } 231 | 232 | } else { //Tabs all fit! 233 | $tabsContainer.removeClass('js-tab-stack'); 234 | } 235 | } 236 | } 237 | 238 | $(window).resize(function() { 239 | if(this.resizeTO) clearTimeout(this.resizeTO); 240 | this.resizeTO = setTimeout(function() { 241 | $(this).trigger('resizeEnd'); 242 | }, 500); 243 | }); 244 | 245 | $(window).bind('resizeEnd', function() { 246 | /*----Convert Responsive Tabs to drop-down if needed ---*/ 247 | checkTabsWidth(); 248 | }); 249 | 250 | $(window).trigger('resizeEnd'); 251 | 252 | 253 | 254 | 255 | //Enable tab changes 256 | $tabsContainer.find('li a').click(function(event) { 257 | event.preventDefault(); 258 | var $self = $(this), 259 | $parent = $self.parent('li'); 260 | 261 | if(!$parent.hasClass('js-tab-toggler')) { 262 | //Find currently visible tab content and hides it 263 | $parent.siblings('.tab-active').each(function() { 264 | currentTabContent = $(this).children('a').attr('href'); 265 | $(currentTabContent).hide(); 266 | }); 267 | $parent.siblings().attr('aria-selected', 'false'); 268 | $parent.siblings().removeClass("tab-active"); 269 | 270 | //Show new tab content 271 | $parent.attr('aria-selected', 'true'); 272 | $parent.addClass("tab-active"); 273 | $(this.hash).fadeIn(); 274 | $self.focus(); 275 | 276 | //Collapse stacked tabs, if they are stacked 277 | if($tabsContainer.hasClass('js-tab-stack-open')){ 278 | toggleTabs(event); 279 | checkTabsWidth(); 280 | } 281 | 282 | } else { 283 | toggleTabs(event); 284 | } 285 | 286 | }); 287 | }; 288 | --------------------------------------------------------------------------------