Tabs
Tabs for a page section, written as plain markup: a row of buttons, one panel each, anything inside a panel, any number of them.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThe look of the preview is thetabs-*classes of the paste. The Embed holds what moves, and reads five variables,--tabs-accent(the plate),--tabs-ink,--tabs-radius,--tabs-lineand--tabs-fade: set them on the block, or on anything above it, in a rule of your own. The box of the panels is bare in the preview; give its class a colour to make it a surface. Keep the padding on the panels, since the box changes height from one to the next. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-tabs Marks the block that holds the row of tabs and the panels. It takes no value: leave the value field empty. The settings below go on the same element. data-tabs-list On the row that holds the tabs. It takes no value. It becomes the tablist, and it is the part that scrolls sideways on a narrow screen. Name it for screen readers with aria-label, "Services" in the paste. Without it the block is left alone and the engine says so in the console. [ Required ] data-tabs-tab On each tab: a button in plain HTML, a Link block in Webflow, its link left on #. It takes no value. The first tab opens the first panel, the second the second: nothing else pairs them. A tab with disabled or aria-disabled="true" is dimmed and skipped. [ Required ] data-tabs-panels On the box that holds the panels and nothing else. It takes no value. Its height follows the open panel during a change, so the padding goes inside the panels. Without it the engine takes the parent of the first panel. [ Optional ] data-tabs-panel On each panel, as many as there are tabs, in the same order. It takes no value. A panel holds anything. Its id, set in the ID field of the Settings panel, is what the address carries: page#delivery opens that tab. [ Required ] data-tabs-state Written by the engine on a panel: active on the open one, leaving on the one that rolls up. The open tab carries aria-selected="true", the row and the box of the panels data-tabs-moving during a change, a row that scrolls data-tabs-fade (start, end or both), the block data-tabs-ready. Never written by hand; the stylesheet reads them, and so can a rule of yours. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-tabs-start 1 The tab that is open when the page arrives, when the address names none. Options: a number counted from 1, 2 for the second tab; or the id of a panel, delivery. data-tabs-activation auto What the arrow keys do in the row of tabs. Options: auto, the arrow moves the focus and opens that tab at once, the usual choice when the panels are already in the page; manual, the arrow only moves the focus and Enter or Space opens the tab, for panels that are heavy to show. data-tabs-hash on Whether the open tab is in the address of the page. Options: on, a tab chosen by the visitor writes the id of its panel after the # without adding a step to the history, and an address or a link that names a panel, or anything inside one, opens it; off, the address is neither read nor written. data-tabs-autoplay 0 The time spent on a tab before the next one opens by itself, in seconds. data-tabs-duration 0.9 The time of a whole change, in seconds: the plate sinks, the line runs, the plate rises, the panels hand over.
Two files, no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
Tabs.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-tabs-start 1 The tab that is open when the page arrives, when the address names none. Options: a number counted from 1, 2 for the second tab; or the id of a panel, delivery. data-tabs-activation auto What the arrow keys do in the row of tabs. Options: auto, the arrow moves the focus and opens that tab at once, the usual choice when the panels are already in the page; manual, the arrow only moves the focus and Enter or Space opens the tab, for panels that are heavy to show. data-tabs-hash on Whether the open tab is in the address of the page. Options: on, a tab chosen by the visitor writes the id of its panel after the # without adding a step to the history, and an address or a link that names a panel, or anything inside one, opens it; off, the address is neither read nor written. data-tabs-autoplay 0 The time spent on a tab before the next one opens by itself, in seconds. Changes sooner; 0 turns it off, which is the default Leaves more time to read. Five seconds or more for a panel with a paragraph data-tabs-duration 0.9 The time of a whole change, in seconds: the plate sinks, the line runs, the plate rises, the panels hand over. Snappier; under 0.5 the line is hardly seen; 0 changes at once Slower and more shown; over 1.3 the tabs feel late on the click -
Drive it from JavaScript
When a script opens a tab, follows which one is open, or starts and stops the autoplay.const element = document.querySelector('[data-tabs]'); element.addEventListener('tabs:shown', (e) => { console.log(e.detail); }); // Follow the open tab: an analytics call, a picture that loads on demand. element.addEventListener('tabs:change', (event) => { console.log(event.detail.index, event.detail.panel.id, event.detail.by); }); const tabs = Tabs.mount(element); // the block auto() mounted, or a new one tabs.select('delivery'); // by the id of a panel, or by a number counted from 0 tabs.next(); // the tab after, round the end // When the tabs leave the page: Tabs.destroy(element); // removes what the engine added, gives the markup back as it was
-
Fit it to your page
Tabs and panels
- The tabs and the panels pair in order: the first tab opens the first panel, the second the second. Nothing else links them.
- To add a tab, add a button marked
data-tabs-tabin the row and a block markeddata-tabs-panelin the box of the panels, at the same place in each. In Webflow, duplicate a tab and a panel in the Navigator. To remove one, delete both. To reorder, move both. - A panel holds anything: text, pictures, a form, a grid, another set of tabs. Panels may have any height; the box follows the open one.
- **Give each panel an
id** in the page's own words (pricing, nottab-2). It is what the address shows, and what a link elsewhere on the page points at:<a href="#delivery">opens that tab and brings the tabs into view. A panel without anidgets one from the engine, which is not written in the address in a form worth sharing. - Name the row:
aria-label="Services"on the element markeddata-tabs-list, oraria-labelledbypointing at a heading. It is what a screen reader says before the tabs. - A tab that cannot be opened carries
disabled(a button) oraria-disabled="true"(a link): it is dimmed, and the arrows skip it. - Several sets on a page, and a set inside a panel of another, each work on their own.
The keyboard
Tab reaches the open tab, and Tab again goes into its panel: to the first link or control in it, or to the panel itself when it has none. In the row, Left and Right go from tab to tab round the ends, Home and End go to the first and the last; with
activationatautothe tab under the focus opens, atmanualEnter or Space opens it. In a page written right to left the two arrows are swapped.The address
With
hashaton, the default:- a tab chosen by the visitor writes
#and theidof its panel in the address, withhistory.replaceState: the Back button still leaves the page in one step; - a page that arrives with a hash naming a panel, a tab, or anything inside a panel opens that tab and scrolls to it;
- a link on the same page to such an
iddoes the same.
A tab opened by the autoplay or by a script writes nothing.
Autoplay
data-tabs-autoplay="6"opens the next tab every six seconds, round the end. A line of ink grows across the head of the open tab to show the time left. The clock holds while the pointer is over the block, while the focus is inside it, while the tabs are off screen and while a change is on its way; it stops for good as soon as a tab is chosen by a click, by the keyboard, by the address or byselect(), andplay()starts it again. Underprefers-reduced-motion: reduceit never starts.On a narrow screen
The row never wraps. When the tabs do not fit it scrolls sideways under the finger, with no scrollbar, the side where tabs are hidden fading out over
--tabs-fade. The open tab is brought to the middle of the row, so the next one is always in sight, and so is a tab that takes the focus.The colours and the sizes
Five custom properties, set at zero specificity on the block. Give them your own values on the block or on anything above it:
--tabs-accent: the plate of the open tab, the line that runs, and the line drawn at the foot of a tab under the pointer.--tabs-ink: the label of the open tab and of a tab under the pointer, the line of the autoplay across the head of the plate, the focus ring of a tab.--tabs-radius: the two top corners of the plate, 2px in the preview.0for a square one.--tabs-line: the thickness of the line the plate sinks to, and of the two other lines.--tabs-fade: the width of the fade at the ends of a row that scrolls.
The colour of a tab at rest, its padding and its type are the tab's own class. The box of the panels has a class of its own: in the preview it is bare, the panels sitting on the ground of the page; give it a colour and it becomes a surface the tabs stand on.
From JavaScript
Tabs.mount(element, overrides)returns the instance of a block, mounting it if it was not:{ element, settings, tabs(), panels(), index(), select(which, options), next(), prev(), play(), pause(), refresh(), destroy() }.selecttakes a number counted from 0, theidof a panel or of a tab, or a tab or panel element, and{ instant: true }to change with no motion; it returnsfalsewhen nothing changed.Tabs.auto(root)mounts every marked block insideroot, and runs once on its own;Tabs.destroy(element)gives the markup back.- Events, all bubbling from the block:
tabs:ready;tabs:changeas soon as a tab is opened, with{ index, previous, tab, panel, by }indetail,bybeinguser,hash,autoplayorapi;tabs:shownonce the plate and the panel have arrived. - The state is in the markup:
aria-selected="true"on the open tab,data-tabs-state="active"on the open panel (leavingon the one that rolls up),data-tabs-movingon the row and on the box of the panels during a change,data-tabs-fade(start,endorboth) on a row that scrolls,data-tabs-readyon the block. - **Under
prefers-reduced-motion: reduce** a tab opens at once: the plate is under the new tab and the new panel is there, with no travel. - Without JavaScript the row is a row of buttons and the panels are all in the page, one under the other, in the order of the tabs.
- The words of the demo are the demo's: on a real page every label and every panel is the user's own, or asked for.
-
Avoid the pitfalls
- As many panels as tabs. They pair in order; an extra tab or an extra panel is left alone and the engine says so in the console.
- **The box marked
data-tabs-panelsholds the panels and nothing else**: its height is animated from one panel to the next. It may be a surface with a colour of its own; put the padding inside the panels. - In Webflow each tab is a Link block, marked
data-tabs-tab: Webflow has no button element a paste can carry, and a Link block takes the focus and the keyboard the same way. Leave its link on#. Do not use Webflow's own Tabs element: it brings its own script and its own classes. - The stylesheet hides the closed panels. Without
tabs.cssevery panel stays in sight and nothing shows which tab is open, and the engine says so in the console. - A tab keeps one line. The row scrolls instead of wrapping, so a very long label makes a very wide tab: keep labels to a few words.
- The plate is behind the label, in the accent. Keep a label colour that reads on both the ground of the page and the accent: the ink of the preview does on amber, a white label would not.
- The line runs at the foot of the row, against whatever is under it. Keep the accent readable on what is under the row, the ground of the page or a surface of the panels.
- A panel that changes height after it opened, a picture that loads late for one, simply grows: the box has no height of its own at rest.
- **An
idused twice on a page** breaks the address: the hash opens the first element that carries it. - **With
hashaton, a linkhref="#some-panel"no longer scrolls to the panel only**: it opens the tab first, then brings the tabs into view. - In React or any framework that owns the DOM, mount in an effect and call
destroy()in its cleanup; the engine adds one element to the row, the plate that travels, and writes attributes on the tabs and panels.
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Tabs is a site utility component for Webflow and vanilla JavaScript, with no library. Also called animated tabs, autoplay tabs or tab indicator animation.
Tabs for a page section, written as plain markup: a row of buttons, one panel each, anything inside a panel, any number of them. The open tab stands on a plate of colour. On a change the plate sinks to a line at the foot of the tab, the line runs along the foot of the row, stretching towards the next tab, and rises there as a plate again; meanwhile the panel that leaves rolls up towards that line and the next one unrolls from it, and the box goes from one height to the other, so the page under the tabs follows and never jumps. A change asked for in the middle of another carries on from where the plate is. The whole ARIA tabs pattern: roles written by the engine, one tab stop, arrows, Home and End, activation on focus or on Enter as a setting, disabled tabs skipped. The open tab is in the address, so a link opens the right one, and a link to anything inside a panel opens that panel. On a narrow screen the row scrolls sideways, fades on the side where tabs are hidden, and keeps the open tab in the middle. An optional autoplay counts on a line across the head of the open tab, holds under the pointer, with the focus inside and off screen, and stops for good when a tab is chosen. Without JavaScript the panels are all in the page, one under the other. Reduced motion respected. No library. Pattern type tabs, tabbed content, section navigation.
Updated
Questions
Paste the block: it arrives as a div marked data-tabs holding a row marked data-tabs-list with one Link block per tab marked data-tabs-tab, and a box marked data-tabs-panels with one div per panel marked data-tabs-panel. The tabs and the panels pair in order, so adding a tab is duplicating one of each in the Navigator.
Yes. data-tabs-autoplay="6" on the block opens the next tab every six seconds, with a line across the head of the open tab counting the time left. The clock holds while the pointer is over the block, while the focus is inside it and while the tabs are off screen, and stops for good once the visitor chooses a tab.
Yes. The engine writes the ARIA tabs pattern itself: tablist, tab and tabpanel roles, aria-selected, aria-controls and one tab stop. Left, Right, Home and End move between tabs, and data-tabs-activation="manual" makes Enter or Space open the tab instead of the arrow.
Yes. Give each panel an id: choosing a tab writes that id after the # of the address without adding a history step, and a page or a link that names the id, or anything inside the panel, opens that tab. data-tabs-hash="off" turns both off.
No. During a change the box of the panels animates from the height of the panel that leaves to the height of the one that comes, so what is under the tabs glides, while the plate of the open tab sinks to a line, runs to the next tab and rises there. At rest the box has no height of its own.