Skip to content

Instantly share code, notes, and snippets.

@cferdinandi
Last active April 23, 2025 02:55
Show Gist options
  • Save cferdinandi/7efb6e7f3fa56f51e8c69757952a3e18 to your computer and use it in GitHub Desktop.
Save cferdinandi/7efb6e7f3fa56f51e8c69757952a3e18 to your computer and use it in GitHub Desktop.
Watch the tutorial at https://youtu.be/_gZjjg0xu9A
<!DOCTYPE html>
<html>
<head>
<title>Bootstrap</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous">
<style type="text/css">
body {
margin: 1em auto;
max-width: 30em;
width: 88%;
}
</style>
</head>
<body>
<h1>Bootstrap</h1>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home-tab-pane" type="button" role="tab" aria-controls="home-tab-pane" aria-selected="true">Merlin</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile-tab-pane" type="button" role="tab" aria-controls="profile-tab-pane" aria-selected="false">Gandalf</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-tab-pane" type="button" role="tab" aria-controls="contact-tab-pane" aria-selected="false">Radagast</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">🪄</div>
<div class="tab-pane fade" id="profile-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">🧙‍♀️</div>
<div class="tab-pane fade" id="contact-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">🐿️</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq" crossorigin="anonymous"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Kraken</title>
<style type="text/css">
body {
margin: 1em auto;
max-width: 30em;
width: 88%;
}
toggle-tabs [role="tablist"] {
list-style: none;
margin: 0 0 2em;
padding: 0;
}
toggle-tabs [role="tablist"] li {
display: inline-block;
}
toggle-tabs [role="tab"] {
color: currentColor;
margin: 0 0 0.25em;
padding: 0.5em 1em;
text-decoration: none;
}
toggle-tabs [role="tab"]:active,
toggle-tabs [role="tab"]:hover {
background-color: #f7f7f7;
}
toggle-tabs [role="tab"][aria-selected="true"] {
background-color: #e5e5e5;
}
</style>
</head>
<body>
<h1>Kraken</h1>
<toggle-tabs>
<ul tabs>
<li><a href="#merlin">Merlin</a></li>
<li><a href="#gandalf">Gandalf</a></li>
<li><a href="#radagast">Radagast</a></li>
</ul>
<div id="merlin">🪄</div>
<div id="gandalf">🧙‍♀️</div>
<div id="radagast">🐿️</div>
</toggle-tabs>
<script>
customElements.define('toggle-tabs', class extends HTMLElement {
/**
* Instantiate the Web Component
*/
constructor () {
// Get parent class properties
super();
// Define properties
this.tabList = this.querySelector('[tabs]');
// Setup UI
this.setup();
}
/**
* Handle events on the Web Component
* @param {Event} event The event object
*/
handleEvent (event) {
this[`on${event.type}`](event);
}
/**
* Handle click events
* @param {Event} event The event object
*/
onclick (event) {
// Only run on tab links
if (!event.target.matches('[role="tab"]')) return;
// Prevent the link from updating the URL
event.preventDefault();
// Ignore the currently active tab
if (event.target.matches('[aria-selected="true"]')) return;
// Toggle tab visibility
this.toggle(event.target);
}
/**
* Handle keydown events
* @param {Event} event The event object
*/
onkeydown (event) {
// Only run for left and right arrow keys
if (!['ArrowLeft', 'ArrowRight'].includes(event.code)) return;
// Only run if element in focus is on a tab
let tab = document.activeElement.closest('[role="tab"]');
if (!tab) return;
// Only run if focused tab is in this component
if (!this.tabList.contains(tab)) return;
// Get the currently active tab
let currentTab = this.tabList.querySelector('[role="tab"][aria-selected="true"]');
// Get the parent list item
let listItem = currentTab.closest('li');
// If right arrow, get the next sibling
// Otherwise, get the previous
let nextListItem = event.code === 'ArrowRight' ? listItem.nextElementSibling : listItem.previousElementSibling;
if (!nextListItem) return;
let nextTab = nextListItem.querySelector('a');
// Toggle tab visibility
this.toggle(nextTab);
nextTab.focus();
}
/**
* Toggle tab visibility
* @param {Node} tab The tab to show
*/
toggle (tab) {
// Get the target tab pane
let tabPane = this.querySelector(tab.hash);
if (!tabPane) return;
// Get the current tab and content
let currentTab = tab.closest('[role="tablist"]').querySelector('[aria-selected="true"]');
let currentPane = document.querySelector(currentTab.hash);
// Update the selected tab
tab.setAttribute('aria-selected', true);
currentTab.setAttribute('aria-selected', false);
// Update the visible tabPane
tabPane.removeAttribute('hidden');
currentPane.setAttribute('hidden', '');
// Make sure current tab can be focused and other tabs cannot
tab.removeAttribute('tabindex');
currentTab.setAttribute('tabindex', -1);
}
/**
* Add buttons and hide content on page load
*/
setup () {
// Only run if there are tabs
if (!this.tabList) return;
// Get the list items and links
let listItems = this.tabList.querySelectorAll('li');
let links = this.tabList.querySelectorAll('a');
// Add ARIA to list
this.tabList.setAttribute('role', 'tablist');
// Add ARIA to the list items
for (let item of listItems) {
item.setAttribute('role', 'presentation');
}
// Add ARIA to the links and content
let instance = this;
links.forEach(function (link, index) {
// Get the the target element
let tabPane = instance.querySelector(link.hash);
if (!tabPane) return;
// Add [role] and [aria-selected] attributes
link.setAttribute('role', 'tab');
link.setAttribute('aria-selected', index === 0 ? true : false);
// If it's not the active (first) tab, remove focus
if (index > 0) {
link.setAttribute('tabindex', -1);
}
// If there's no ID, add one
if (!link.id) {
link.id = `tab_${tabPane.id}`;
}
// Add ARIA to tab pane
tabPane.setAttribute('role', 'tabpanel');
tabPane.setAttribute('aria-labelledby', link.id);
// If not the active pane, hide it
if (index > 0) {
tabPane.setAttribute('hidden', '');
}
});
// Listen for events
this.tabList.addEventListener('click', this);
document.addEventListener('keydown', this);
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Web Awesome</title>
<link rel="stylesheet" href="https://early.webawesome.com/[email protected]/dist/styles/themes/default.css" />
<link rel="stylesheet" href="https://early.webawesome.com/[email protected]/dist/styles/webawesome.css" />
<style type="text/css">
body {
margin: 1em auto;
max-width: 30em;
width: 88%;
}
</style>
</head>
<body>
<h1>Web Awesome</h1>
<wa-tab-group>
<wa-tab panel="merlin">Merlin</wa-tab>
<wa-tab panel="gandalf">Gandalf</wa-tab>
<wa-tab panel="radagast">Radagast</wa-tab>
<wa-tab-panel name="merlin">🪄</wa-tab-panel>
<wa-tab-panel name="gandalf">🧙‍♀️</wa-tab-panel>
<wa-tab-panel name="radagast">🐿️</wa-tab-panel>
</wa-tab-group>
<script type="module" src="https://early.webawesome.com/[email protected]/dist/webawesome.loader.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Relative Time - Kraken</title>
<style type="text/css">
body {
margin: 1em auto;
max-width: 30em;
width: 88%;
}
</style>
</head>
<body>
<h1>Relative Time - Kraken</h1>
<relative-time>2025-07-04T09:17:00-04:00</relative-time>
<script>
customElements.define('relative-time', class extends HTMLElement {
/**
* Instantiate the Web Component
*/
constructor () {
// Get parent class properties
super();
// Get attributes
let dateStr = this.textContent;
let date = Date.parse(dateStr);
this.date = Number.isNaN(date) ? Date.now() - 1 : date;
this.locale = this.getAttribute('lang') ?? 'en-US';
let sync = this.hasAttribute('sync');
// Create <time> element
this.time = document.createElement('time');
this.time.setAttribute('datetime', dateStr);
this.innerHTML = '';
this.append(this.time);
// Format relative time
this.time.textContent = this.format();
if (sync) {
setInterval(() => {
this.time.textContent = this.format();
}, 1000 * 60);
}
}
format () {
let options = this.getOptions();
let formatter = new Intl.RelativeTimeFormat(this.locale, {
style: this.getAttribute('type') ?? undefined,
numeric: this.getAttribute('numeric') ?? undefined,
});
return formatter.format(...options);
}
getOptions () {
// Date and elapsed time
let now = Date.now();
let elapsed = this.date - now;
// Units in miliseconds
let units = [
['year', 24 * 60 * 60 * 1000 * 365],
['month', 24 * 60 * 60 * 1000 * 365/12],
['day', 24 * 60 * 60 * 1000],
['hour', 60 * 60 * 1000],
['minute', 60 * 1000],
['second', 1000]
];
// "Math.abs" accounts for both "past" & "future" scenarios
for (let [unit, ms] of units) {
if (Math.abs(elapsed) > ms || unit === 'second') {
return [Math.round(elapsed / ms), unit];
}
}
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Relative Time - Web Awesome</title>
<link rel="stylesheet" href="https://early.webawesome.com/[email protected]/dist/styles/themes/default.css" />
<link rel="stylesheet" href="https://early.webawesome.com/[email protected]/dist/styles/webawesome.css" />
<style type="text/css">
body {
margin: 1em auto;
max-width: 30em;
width: 88%;
}
</style>
</head>
<body>
<h1>Relative Time - Web Awesome</h1>
<wa-relative-time date="2025-07-04T09:17:00-04:00"></wa-relative-time>
<script type="module" src="https://early.webawesome.com/[email protected]/dist/webawesome.loader.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment