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

              
                // Design objectives
// - Responsive tabbed navigation with subtle/meaningful transitions
// - Alignment of tooltips changes based on position
// - Consistent weight for text, icons and underline
// - Top-left 'lighting' of 3D icons in both dark and light themes

// Technical objectives
// - Simple HTML structure
// - Allow for cacheable (but still stylable) vector assets in production
// - 'Compact' breakpoint determined by content
 
// Solution notes
// - js ResizeObserver used to trigger toggle of 'compact' class
// - Whilst width is not animatable, max-width is. JS applies max-width properties only when a nav change is made to avoid weirdness when the collapse breakpoint is crossed. Overflow: hidden also only applied during change so as to not compromise display of tooltips at other times
// - Fraction pixel box shadow used to thicken rounded end underline (to match text and icon weight)
// - Strokes of both text (via -webkit-text-stroke) and SVG (via stroke-width) thicken when nav link clicked
// - Psuedo element / data attribute driven tooltips using a combination of absolute and flex/grid self alignment/justification
// - See https://codepen.io/scootman/pen/MWmombb & https://twitter.com/_AntiAlias_/status/1416989388429352961?s=20 for 3D SVG icon approach



body data-theme='light'	
	
	// svg assets (in production these would be in a file e.g. icons.svg )
	[[[https://codepen.io/scootman/pen/XWyBZva]]]


	main data-width='fixed'
	
		nav.tabs#demo 
		
			a.active data-tooltip='Observers'
				svg
					use href='#person'
					// in production this would be something like 'icons.svg#person'
				' Observers
				
			a data-tooltip='Species'	
				svg
					use href='#taxa'
				' Species
				
			a data-tooltip='Locations'		
				svg
					use href='#location'
				' Locations	
				
			a data-tooltip='Expeditions'		
				svg
					use href='#expedition'
				' Expeditions			
				
				
		.action-set.segmented#theme
		
			input#light-theme (type="radio" name="theme" value="light" checked)
			label for="light-theme" data-tooltip='Light theme'
				svg
					use href='#view-light'
					
			input#dark-theme (type="radio" name="theme" value="dark")
			label for="dark-theme" data-tooltip='Dark theme'
				svg
					use href='#view-dark'				
				
				
		.action-set.segmented#width
		
			input#width-fixed (type="radio" name="width" value="fixed" checked)
			label for="width-fixed" data-tooltip='Fixed mobile width'
				svg
					use href='#width-fixed-narrow'
					
			input#width-responsive (type="radio" name="width" value="responsive")
			label for="width-responsive" data-tooltip='Responsive width'
				svg
					use href='#width-variable'		
				
              
            
!

CSS

              
                // This demo loads this base scss file: https://codepen.io/scootman/pen/xxQJWxY


$nav-tabs-dims: (
	item-gap: 22px,
	item-base-h: 24px,
	item-contents-gap: 8px,
	item-bleed-y: 6px,
	line-offset: 10px
);

@function nav-tabs-dim($property) {
	@return map-get($nav-tabs-dims, $property);
}


body {
	font-family: barlow;
	background: ink-color(5);
	color: ink-color();
	display: flex;
	justify-content: center;
	min-height: 100vh;
	padding: 18px;
}

main {
	position: relative;
	flex: 1 0 339px;
	max-width: 732px;
	display: grid;
	grid-template-rows: minmax(auto, 1fr) auto 5vh minmax(auto, 1fr); // extra 5vh row added to put nav at optical center
	grid-template-columns: auto;
	grid-template-areas: 
		"header"
    "content"
		"."
    "footer";
	
	transition: all .25s ease-in-out;
	
	&[data-width='fixed'] {
		max-width: 375px;
	}
}

@mixin nav-tabs-item-strong {
	--item-fg-color: #{ink-color()};
	--item-bg-lighter-color: var(--ink-lighter-color);
	--item-bg-darker-color: var(--ink-darker-color);
}

nav.tabs#demo {
	grid-area: content;

}

nav.tabs {
	position: relative;
	display: flex;
	justify-content: center;

	margin: -#{nav-tabs-dim(item-bleed-y)} -#{nav-tabs-dim(item-gap) / 2} 0;
	height: nav-tabs-dim(item-base-h) + nav-tabs-dim(line-offset) + nav-tabs-dim(item-bleed-y) + 1px;

	font-size: 16.5px;
	font-weight: 500;
	line-height: nav-tabs-dim(item-base-h) - 2px;

	&::before {
		// underline
		content: "";
		position: absolute;
		height: 1px;
		left: nav-tabs-dim(item-gap) / 2;
		right: nav-tabs-dim(item-gap) / 2;
		bottom: 0px;
		border-radius: 0.5px;
		
		color: ink-color(20);
		background: currentcolor;
		box-shadow: 0 0 0 0.25px currentcolor;
		
		transform: translatey(-0.5px); // this serves 2 purposes: solves a Firefox rendering issue and means anti-aliasing on low-ppi devices is spread across only 2px vertically not 3px
	}

	a {
		--item-fg-color: #{ink-color(35)};
		--item-bg-lighter-color: var(--ink-lighter-inactive-color);
		--item-bg-darker-color: var(--ink-darker-inactive-color);

		position: relative;
		box-sizing: content-box;
		display: grid;
		// grid gives more options to absolutely positioned elements (like the tooltips) than flexbox
		
		height: nav-tabs-dim(item-base-h);
		margin: 0 0 -#{nav-tabs-dim(item-bleed-y)};
		border-block: nav-tabs-dim(item-bleed-y) transparent solid;
		border-inline: nav-tabs-dim(item-gap) / 2 transparent solid;
		// use of border rather than padding helps for tooltip alignment

		grid-template-columns: auto auto;
		column-gap: nav-tabs-dim(item-contents-gap);

		color: var(--item-fg-color);

		-webkit-user-select: none;
		user-select: none;
		cursor: pointer;
		transition: max-width 0.25s ease-in-out, color 0.1s ease-in-out;
		// max width must be first for JS to interogate

		&::before {
			// underline
			content: "";

			position: absolute;
			top: nav-tabs-dim(item-base-h) + nav-tabs-dim(line-offset);
			left: 0;
			right: 0;
			height: 1px;
			border-radius: 0.5px;
			
			color: transparent;
			background: currentcolor;
			box-shadow: 0 0 0 0.25px currentcolor;
			
			transform: scalex(0) translatey(-0.5px);
			transition: all 0.1s ease-in-out;
			pointer-events: none;
		}
		
		&::after {
			display: none;
		}
		
		&:last-child {
			@include tooltip-anchor(bottom-right); // this ensures tooltips arent off-screen on small devices
		}
		
		&:first-child {
			@include tooltip-anchor(bottom-left);
		}
		
		&.transition {
			pointer-events: none;
			overflow-x: hidden;
			height: nav-tabs-dim(item-base-h) + nav-tabs-dim(line-offset) + 2px;
			&:not(.active) {
				-webkit-text-fill-color: currentcolor;
			}
			&::after {
				display: none;
			}
		}

		&.active {
			@include nav-tabs-item-strong;
			pointer-events: none;
			
			&::before {
				color: var(--item-fg-color);
				transform: scalex(1) translatey(-0.5px);
			}
			
			&::after {
				display: none;
			}
		}

		&:hover {
			@include nav-tabs-item-strong;
		}

		&:active {
			svg {
				stroke-width: 4px;
			}
			
		}

		svg {
			pointer-events: none;

			height: nav-tabs-dim(item-base-h);
			aspect-ratio: 1;
			stroke: var(--item-fg-color);
			color: var(--item-bg-lighter-color);
			fill: var(--item-bg-darker-color);

			transition: all 0.1s ease-in-out;
		}
	}

	&.compact a:not(.active):not(.transition) {
		
		// solution for hiding text (but still allowing overflow for tooltips) without wrapping the text in another HTML element
		-webkit-text-fill-color: transparent;
		grid-template-columns: nav-tabs-dim(item-base-h);
		justify-items: center;
		
		font-size: 1px;
		line-height: 1em;
		
		&::after {
			display: block;
		}
		
	}

	&:not(.compact) a:active {
		-webkit-text-stroke: 0.035em currentcolor;
	}

}


.action-set.segmented#theme {
	justify-self: center;
	align-self: start;
	grid-area: header;
}

.action-set.segmented#width {
	justify-self: center;
	align-self: end;
	grid-area: footer;
	label {
		@include tooltip-anchor(top);
	}
}

              
            
!

JS

              
                const navTabs = document.querySelector('nav.tabs')
const navTabsItems = navTabs.querySelectorAll('a')
const navTabsFirstItem = [...navTabsItems].at(0)
const navTabsLastItem = [...navTabsItems].at(-1)

// store max overall width of items plus min width of an item
const itemsWidth = Math.ceil(navTabsLastItem.getBoundingClientRect().right - navTabsFirstItem.getBoundingClientRect().left)
navTabs.setAttribute('data-items-width', itemsWidth)
const itemMinWidth = navTabsLastItem.clientHeight

// store max width of each item
navTabsItems.forEach(item => {
	// console.log(item)
	item.setAttribute('data-max-width', item.clientWidth)
})

// check initial width and apply compact class if required
if (navTabsLastItem.getBoundingClientRect().right >= navTabs.getBoundingClientRect().right) {
	navTabs.classList.add('compact')
}



// resize compact check
const navTabsObserver = new ResizeObserver(
	(entries, observer) => {
		entries.forEach(entry => {
			if (entry.target.classList.contains('compact') && entry.contentBoxSize[0].inlineSize >= entry.target.dataset.itemsWidth) {
				entry.target.classList.remove('compact')
			} else if (!entry.target.classList.contains('compact') && entry.contentBoxSize[0].inlineSize < entry.target.dataset.itemsWidth) {
				entry.target.classList.add('compact')
			}
	})
})

navTabsObserver.observe(navTabs)



// active tab change

function maxWidth(width) {
	return 'max-width: ' + width  + 'px'
}

navTabs.addEventListener('click', (e) => {
	const changeTo = e.target
	if (changeTo.tagName == 'A') {
		const changeFrom = changeTo.parentNode.querySelector('.active')
		
		if (navTabs.classList.contains('compact')) {
			// compact
			const changeTime = parseFloat(getComputedStyle(changeTo).transitionDuration).toFixed(3)
			const changeFromMaxWidth = changeFrom.dataset.maxWidth
			const changeToMaxWidth = changeTo.dataset.maxWidth
			
			changeTo.setAttribute('style',maxWidth(itemMinWidth) )
			changeTo.classList.add('active', 'transition');
			changeFrom.setAttribute('style',maxWidth(changeFromMaxWidth) )
			changeFrom.classList.remove('active');
			changeFrom.classList.add('transition');

			changeTo.offsetWidth
			changeFrom.offsetWidth
			// forces browser to recognise change with regard to transitions

			changeTo.setAttribute('style',maxWidth(changeToMaxWidth) )
			changeFrom.setAttribute('style',maxWidth(itemMinWidth) )

			setTimeout(() => { 
				changeTo.classList.remove('transition');
				changeTo.setAttribute('style','')
				changeFrom.classList.remove('transition');
				changeFrom.setAttribute('style','')
			}, changeTime * 1000);
			
		} else {
			// default
			changeFrom.classList.remove('active')
			changeTo.classList.add('active')
		}
		
	}
})



// theme control

document.getElementById('theme').addEventListener('change', (e) => {
	document.querySelector('[data-theme]').setAttribute('data-theme', e.target.value)
});

// width control

document.getElementById('width').addEventListener('change', (e) => {
	document.querySelector('[data-width]').setAttribute('data-width', e.target.value)
});



              
            
!
999px

Console