Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                - const BASE = 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/';
- const DATA = [ // tabs content data
- 	{
-			name: 'slider', 
- 		desc: 'Define a slider others can update.', 
- 		link: 'input/range'
- 	}, 
- 	{
-			name: 'number input', 
- 		desc: 'Create a number.', 
- 		link: 'input/number'
- 	}, 
- 	{
-			name: 'table', 
- 		desc: 'Create a table to input data.', 
- 		link: 'table'
- 	}
- ];
- const N = DATA.length; // number of tabs

- let k = 0; // selected tab index on page load
- let r = 5; // tab rounding

style
	- for(let i = 0; i < N; i++)
		- let j = i + 1;
		| /* tab and tab panel indices */
		| [role^=tab]:nth-of-type(#{j}) { --i: #{i} }
		| /* tab panel flag based on corresponding tab 
		|  * being hovered or focused */
		|	:has([role=tab]:nth-child(#{j}):is(:hover, :focus)) 
		| [role=tabpanel]::nth-of-type(#{j}) { --hov: 1 }

svg(width='0' height='0' aria-hidden='true')
	filter#roundstroke(color-interpolation-filters='sRGB')
		// extract just the semi-transparent tab "border-color"
		// and make it go from alpha = .5 to fully opaque
		feComponentTransfer
			feFuncA(type='table' tableValues='0 1 0')
		// dilate it to make it intersect tab shape edges
		feMorphology(operator='dilate' radius=2*r result='line')
		// extract just the fully opaque no rounding tab shape
		feComponentTransfer(in='SourceGraphic')
			feFuncA(type='table' tableValues='0 0 1')
		// blur has the side effect of rounding corners
		feGaussianBlur(stdDeviation=r result='blur')
		// also creates semitransparent edge pixels
		// push alpha of most (not all) towards either 0 or 1
		// whichever is closer, using out of bounds values
		// depending on the r value; save as 'fill'
		// this gives us the rounded tab
		feComponentTransfer(result='fill')
			feFuncA(type='table' 
			        tableValues=`${-1*Math.round(Math.sqrt(2*r))} ${r + 1}`)
		// now for the tab border
		// push alpha of most (not all) semitransparent pixels
		// towards either 0 or 1 as follows:
		// close to either 0 or 1 => push towards 0
		// close to .5 => push towards 1
		feComponentTransfer(in='blur')
			feFuncA(type='table' 
			        tableValues=[-r, r, -r].join(' '))
		// keep the "border-color" area only within this shape
		feComposite(in='line' operator='in')
		// place the no line rounded tab on top
		feBlend(in='fill')

main
	h1#title Tabs with rounded corners & borders
	section.tabs(style=`--n: ${N}; --k: ${k}; --r: ${r}px`)
		nav.tablist(role='tablist' aria-labelledby='title')
			- for(let i = 0; i < N; i++)
				- let c = DATA[i];
				button(role='tab' 
				       id=`tab${i}` aria-controls=`panel${i}` 
				       aria-selected=i ? 'false' : 'true' 
							 tabindex=i ? -1 : 0) #{c.name}
		- for(let i = 0; i < N; i++)
			- let c = DATA[i];
			div(role='tabpanel' 
			    id=`panel${i}` aria-labelledby=`tab${i}` 
			    aria-hidden=i ? 'true' : 'false')
				.back(aria-hidden='true')
				.content
					p #{c.desc} 
						a(href=`${BASE}${c.link}` target='_blank') Link
						| .
              
            
!

CSS

              
                @import url('https://fonts.googleapis.com/css2?family=Kode+Mono:wght@400..700&family=Shantell+Sans:wght@300..800&display=swap');

/* grid 'em all  */
html, body, main, section, nav, div { display: grid }

html { min-height: 100% }

body {
	place-content: center;
	grid-template-columns: Min(100%, 40em);
	overflow-x: hidden; /* juust in case */
	/* image background to show there are no covers */
	background: 
		url(https://images.unsplash.com/photo-1534577403868-27b805ca4b9c?w=2400) 
			50%/ cover #191c1f;
	background-blend-mode: multiply;
	color: #ededed; /* contrasting text */
	font: 300 clamp(.75em, 6.25vw, 1.5em)/ 1.125 
		shantell sans, cursive
}

/* svg only to contain filter which alters graphic result
 * so it's functionally the same as a style element, 
 * take it out of document flow */
svg[aria-hidden='true'] { position: fixed }

h1 {
	font-weight: 500;
	text-align: center;
	text-wrap: balance
}

a, button, div { --hov: 0 }

a { color: limegreen }

:focus { outline: dotted 1px }

:is(a, button):is(:hover, :focus) { --hov: 1 }

.tabs {
	/* no gap between tab & corresponding tab panel (row)
	 * only between tabs (column gap) */
	grid-gap: 0 Max(.375em, calc(2*var(--r)));
	grid-template: 
		repeat(2, max-content)/ 
		Max(5%, 3*var(--r)) /* first tab offset */ 
		/* the n tabs, sized by their content */
		repeat(var(--n), max-content) 
		1fr /* space after last tab */;
	font-family: kode mono, monospace
}

[role='tablist'] {
	/* ensure it's on top of selected tab panel */
	z-index: 2;
	/* make it occupy the entire first row */
	grid-area: 1/ 1/ span 1/ -1;
	/* and inherit columns */
	grid-template-columns: subgrid
}

[role*='tab'] {
	/* difference between selected tab & this tab index */
	--dif: var(--k) - var(--i);
	/* NOT selected flag; 
	 * 0 if selected (ON), 1 if not selected (OFF) */
	--not: Min(1, Max(var(--dif), -1*(var(--dif))));
	/* is selected flag; 
	 * 1 if selected (ON), 0 if not selected (OFF) */
	--sel: calc(1 - var(--not));
	/* percentage value based on this flag
	 * 100% if selected, 0% if not */
	--prc: calc(var(--sel)*100%);
	color: /* based on is selected flag */
		color-mix(in srgb, 
			/* sel ON, hov irrelevant */
			#04e4a4 var(--prc), 
			/* sel OFF, based on is hovered flag */
			color-mix(in srgb, 
					#746c76 /* hov ON */ calc(var(--hov)*100%), 
					#3d4144 /* hov OFF */))
}

[role='tab'] {
	/* column depending on its index */
	grid-area: 1/ calc(var(--i) + 2);
	overflow: hidden; /* prevent spillout if needed */
	/* focus ring space in from border */
	outline-offset: calc(-2*var(--r));
	border: none; /* override browser default */
	padding: .5em .75em; /* give the text some space */
	/* limit its width */
	max-width: Max(3em, 100vw/(var(--n) + 1));
	background: none; /* override browser default */
	font: inherit; /* override browser default */
	font-weight: 700; /* make it stand out */
	/* some refining touches */
	text-overflow: ellipsis;
	text-transform: capitalize;
	white-space: nowrap;
	cursor: pointer
}

[role='tabpanel'] {
	/* make it occupy entire grid */
	grid-area: 1/ 1/ -1/ -1;
	/* and inherit both rows and columns */
	grid-template: subgrid/ subgrid;
	/* sepends on selection status */
	z-index: var(--sel)
}

.back {
	/* make it cover entire parent grid */
	grid-area: 1/ 1/ -1/ -1;
	/* and inherit entire parent grid, both rows & cols */
	grid-template: subgrid/ subgrid;
	/* cancel out padding */
	margin: calc(-1*var(--r));
	/* make some space around its pseudos 
	 * that create the basic no rounding tab shape */
	padding: var(--r);
	/* semitransparent outer area outside pseudos 
	 * this gets extrated to create the border  
	 * using its RGB channels */
	background: 
		color-mix(in srgb, currentcolor, #0000);
	/* this rounds the shape created by the pseudos
	 * and gives it a border */
	filter: url(#roundstroke);
	
	&::before, &::after {
		background: 
			/* depends on selection status */
			color-mix(in srgb, 
				#101214 var(--prc) /* when selected */, 
				#202428 /* when not selected */);
		content: ''
	}
	
	/* put it in same cell as corresponding tab (button) */
	&::before { grid-area: 1/ calc(var(--i) + 2) }
	
	/* make it cover entire second row (content area) */
	&::after { grid-area: 2/ 1/ -1/ -1 }
}

.content {
	/* make it occupy second row */
	grid-area: 2/ 1/ span 1/ -1;
	align-content: start;
	/* this text content on top of its back sibling 
	 * responsible for background and border */
	z-index: 1;
	padding: .5em;
	min-height: 9em;
	color: #dedede;
	
	[role='tabpanel'][aria-hidden='true'] & {
		display: none
	}
}
              
            
!

JS

              
                const _TABS = document.querySelector('.tabs'), 
			_LIST = [..._TABS.querySelectorAll('button[role=tab]')], 
			_PANE = [..._TABS.querySelectorAll('[role=tabpanel]')], 
			N = _LIST.length;

let k = _TABS.style.getPropertyValue('--k');

function switchAttr(e1, e2, attr) {
	[e1[attr], e2[attr]] = [e2[attr], e1[attr]]
}

function switchTabs(i) {
	switchAttr(_LIST[i], _LIST[k], 'ariaSelected');
	switchAttr(_PANE[i], _PANE[k], 'ariaHidden');
	
	/* for some reason, switching like above doesn't work */
	_LIST[i].tabIndex = 0;
	_LIST[k].tabIndex = -1;
	
	_LIST[i].focus();
	_TABS.style.setProperty('--k', k = i)
}

addEventListener('click', e => {
	let _t = e.target, i = _LIST.indexOf(_t);
	
	if(i > -1) switchTabs(i)
});

addEventListener('keyup', e => {
	let _t = e.target, i = _LIST.indexOf(_t);
	
	if(i !== -1) {
		console.log(e.keyCode)
		switch(e.keyCode) {
			case 39: // ->
				switchTabs((k + 1)%N);
				break;
			case 37: // <-
				switchTabs((k + N - 1)%N);
				break;
			case 36: // HOME
				switchTabs(0);
				break;
			case 35: // END
				switchTabs(N - 1);
				break;
		}
	}
});
              
            
!
999px

Console