<section class="section">
<h2>First Instance Webcomponent.</h2>
<h2>Safari decides not to support Web components</h2>
<iga-tab active="2" style="--color-tab-active-background: #e66c4d">
<div slot="group-tabs" role="tablist">
<iga-tab-item>
<span slot="item">Tab 1</span>
</iga-tab-item>
<iga-tab-item>
<span slot="item">Tab 2</span>
</iga-tab-item>
<iga-tab-item>
<span slot="item">Third tab</span>
</iga-tab-item>
<iga-tab-item></iga-tab-item>
</div>
<main slot="group-panels" class="tabs__panels">
<iga-tab-panel>
<div slot="panel">
<p>
1 Lorem, ipsum consectetur dolor sit amet consectetur
adipisicing elit. Quia deleniti quisquam similique a rerum.
</p>
<p>
2 Lorem ipsum dolor sit amet c commodi, harum distinctio nulla
quibusdam dolorum consequatur minus. Quibusdam, sit?
</p>
</div>
</iga-tab-panel>
<iga-tab-panel>
<div slot="panel">
<p>
3 Lorem, ipsum dolor sit actetur adipisicing elit. Quia deleniti
quisquam similique a rerum.
</p>
<p>
4 Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint
maxime commodi, harum distinctio nulla quibusdam dolorum
consequatur minus. Quibusdam, sit?
</p>
</div>
</iga-tab-panel>
<iga-tab-panel>
<div slot="panel"></div>
</iga-tab-panel>
<iga-tab-panel>
<div slot="panel">
<p>5 Lorem ipsum dolor sit amet c commodi, harum</p>
<img src="https://dummyimage.com/560x180/D2B48C/fff" alt="" />
</div>
</iga-tab-panel>
</main>
</iga-tab>
</section>
<section class="section">
<h2>Second Instance Webcomponent.</h2>
<h2>Safari decides not to support Web components</h2>
<iga-tab active="0" justify="space-evenly">
<div slot="group-tabs" role="tablist">
<iga-tab-item>
<span slot="item">This That</span>
</iga-tab-item>
<iga-tab-item>
<span slot="item">That Those</span>
</iga-tab-item>
<iga-tab-item>
<span slot="item">Last Tab</span>
</iga-tab-item>
</div>
<main slot="group-panels" class="tabs__panels">
<iga-tab-panel>
<div slot="panel">
<h1>This is panel 1</h1>
<p>
1 Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quia
deleniti quisquam similique a rerum.
</p>
<p>
2 Lorem ipsum dolot maxime commodi, harum distinctio nulla
quibusdam dolorum consequatur minus. Quibusdam, sit?
</p>
</div>
</iga-tab-panel>
<iga-tab-panel>
<div slot="panel">
<p>
3 Lorem, ipsum dolor sit actetur adipisicing elit. Quia deleniti
quisquam similique a rerum.
</p>
<p>
4 Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint
maxime commodi, harum distinctio nulla quibusdam dolorum
consequatur minus. Quibusdam, sit?
</p>
</div>
</iga-tab-panel>
<iga-tab-panel>
<div slot="panel"></div>
</iga-tab-panel>
</main>
</iga-tab>
</section>
body {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
img {
max-width: 100%;
}
@media (min-width: 1200px) {
body {
flex-wrap: nowrap;
}
}
h1,
h2 {
text-align: center;
}
[aria-selected="true"] {
--color-tab-active-foreground: #fff;
}
[slot="panel"]:empty {
--width: 100%;
height: 250px;
background: linear-gradient(0.75turn, #fff, transparent),
linear-gradient(#eee, #eee),
radial-gradient(38px circle at 19px 19px, #eee 50%, transparent 51%),
linear-gradient(#eee, #eee);
background-repeat: no-repeat;
background-size: var(--width) 250px, var(--width) 150px, 100px 100px,
225px 30px;
background-position: var(--width) 0, 0 0, 0px 160px, 50px 165px;
}
class IgaTabItem extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.render();
}
static get observedAttributes() {
return ["tab"];
}
attributeChangedCallback(name, oldValue, newValue) {
const TAB_NAMES = {
tab: newValue,
};
this.tab = TAB_NAMES[name] || 0;
}
connectedCallback() {
this.addEventListener("click", this._clickedEvent.bind(this));
}
disconnectedCallback() {
this.removeEventListener("click", this._clickedEvent.bind(this));
}
get style() {
return `
<style>
:host button {
all: unset;
display: revert;
box-sizing: border-box;
width: 100%;
cursor: pointer;
padding: 10px 15px;
color: var(--color-tab-active-foreground, #414141);
border-radius: 3px;
outline: 1px solid transparent;
outline-offset: -3px;
transition: color var(--trans-dur, .2s) linear var(--trans-del, .2s), outline var(--trans-dur, .2s) linear var(--trans-del, .2s);
}
:host button:hover,
:host button:focus-within {
outline: 2px solid var(--color-tab-active-background, #5A3A31);
}
:host([aria-selected="true"]) {
pointer-events: none;
}
:host([aria-selected="true"]) button {
cursor: default;
}
</style>
`;
}
render() {
this.shadowRoot.innerHTML = `
${this.style}
<button type="button"><slot name="item">Default Tab</slot></button>
`;
}
_clickedEvent() {
this.dispatchEvent(
new CustomEvent("tab-clicked", {
bubbles: true,
detail: { tab: () => this.tab },
})
);
}
}
customElements.define("iga-tab-item", IgaTabItem);
class IgaTabPanel extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "closed" });
}
connectedCallback() {
this.render();
}
get style() {
return `
<style>
:host {
grid-column: 1/-1;
grid-row: 1/-1;
opacity: 0;
visibility: hidden;
transform: scale(0.6);
transition: all var(--trans-dur) linear var(--trans-del);
}
:host([active-panel="true"]) {
opacity: 1;
visibility: visible;
transform: scale(1);
transition: all var(--trans-dur) linear var(--trans-del);
}
</style>
`;
}
render() {
this.shadow.innerHTML = `
${this.style}
<article class="tabs__panel">
<slot name="panel">Default Panel Content</slot>
</article>
`;
}
}
customElements.define("iga-tab-panel", IgaTabPanel);
class IgaTab extends HTMLElement {
active = 0;
resizeTimer;
constructor() {
super();
this.attachShadow({ mode: "open" });
this.render();
this.activeTabBG = this.shadowRoot.querySelector(".js-active-tab-bg");
this.slotsDOM();
}
get justify() {
return this.getAttribute("justify") || "space-between";
}
static get observedAttributes() {
return ["active"];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "active":
this.active = newValue || 0;
break;
}
}
connectedCallback() {
this.handlerEvents();
}
disconnectedCallback() {
this.removeEvents();
}
slotsDOM() {
const slots = this.shadowRoot.querySelectorAll("slot");
slots.forEach((slot) => {
slot.addEventListener("slotchange", (event) => {
const slot = event.target;
if (slot.name == "group-tabs") {
this.tabs = [slot.assignedNodes()[0].children];
this.tabs.forEach((tab, i) => tab.setAttribute("tab", i));
if (this.active >= this.tabs.length)
alert("Has indicado un Tab activo que no existe");
}
if (slot.name == "group-panels") {
this.panels = [slot.assignedNodes()[0].children];
this.setActiveTab();
}
});
});
}
setAttrs() {
this.setTabAttrs();
this.setPanelAttrs();
}
setPanelAttrs() {
this.panels.forEach((panel) => panel.setAttribute("active-panel", false));
this.panels[this.active].setAttribute("active-panel", true);
}
setTabAttrs() {
this.tabs.forEach((tab) => tab.setAttribute("aria-selected", false));
this.tabs[this.active].setAttribute("aria-selected", true);
}
setActiveTab() {
this.setAttrs();
this.setAnimations();
}
setAnimations() {
const [decorWidth, decorHeight, decorOffsetX, decorOffsetY] =
this.findActiveTabParams();
this.styleActiveTabBG(decorWidth, decorHeight, decorOffsetX, decorOffsetY);
}
findActiveTabParams() {
const activeTab = this.tabs[this.active];
const activeItemWidth = activeTab.offsetWidth;
const activeItemHeight = activeTab.offsetHeight;
const activeItemOffsetLeft = activeTab.offsetLeft;
const activeItemOffsetTop = activeTab.offsetTop;
return [
activeItemWidth,
activeItemHeight,
activeItemOffsetLeft,
activeItemOffsetTop,
];
}
styleActiveTabBG(decorWidth, decorHeight, decorOffsetX, decorOffsetY) {
this.activeTabBG.style.width = `${decorWidth}px`;
this.activeTabBG.style.height = `${decorHeight}px`;
this.activeTabBG.style.transform = `translate(${decorOffsetX}px, ${decorOffsetY}px)`;
}
get style() {
return `
<style>
:host {
--trans-dur: 0.2s;
--trans-del: 0.15s;
}
:host(.resize-animation-stopper) {
--trans-dur: 0.01s;
--trans-del: 0.01s;
}
:host *:where(:not(iframe, canvas, img, svg, video):not(svg *)) {
all: unset;
display: revert;
box-sizing: border-box;
}
:host .tabs {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.5;
letter-spacing: 0.01rem;
font-weight: 300;
font-size: 1.1rem;
width: min(98%, 600px);
min-height: 300px;
margin: auto;
background-color: #fff;
border-radius: 3px;
padding: clamp(1rem, 2.5vw, 3rem);
border: 1px solid #d8d8d8;
filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.2));
}
:host .tabs__nav {
position: relative;
}
::slotted([slot="group-tabs"]) {
display: grid;
grid-auto-flow: column;
place-items: center;
justify-content: ${this.justify};
gap: 10px;
margin-block-end: 2rem;
position: relative;
z-index: 2;
}
:host p {
margin-block-end: 1rem;
}
:host .js-active-tab-bg {
position: absolute;
top: 0;
left: 0;
height: 100%;
transition: width var(--trans-dur) linear var(--trans-del), height var(--trans-dur) linear var(--trans-del),
transform var(--trans-dur) ease-out var(--trans-del);
background-color: var(--color-tab-active-background, #5A3A31);
border-radius: 3px;
z-index: 1;
}
::slotted(.tabs__panels) {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
</style>
`;
}
render() {
this.shadowRoot.innerHTML = `
${this.style}
<section class="tabs">
<nav class="tabs__nav">
<slot name="group-tabs"><mark style="all:initial;background-color: yellow;">slot="group-tabs"</mark> needed in your HTML</slot>
<div class="js-active-tab-bg"></div>
</nav>
<slot name="group-panels"><mark style="all:initial;background-color: yellow;">slot="group-panels"</mark> needed in your HTML</slot>
</section>
`;
}
handlerEvents() {
this.addEventListener("tab-clicked", this._clickedEvent.bind(this));
window.addEventListener("resize", this._resizeEvent.bind(this));
window.addEventListener("load", this._loadEvent.bind(this));
}
removeEvents() {
this.removeEventListener("tab-clicked", this._clickedEvent.bind(this));
window.removeEventListener("resize", this._resizeEvent.bind(this));
window.removeEventListener("load", this._loadEvent.bind(this));
}
_clickedEvent(event) {
this.active = event.detail.tab();
this.setActiveTab();
}
_resizeEvent() {
this.setAnimations();
this.resizeAnimationStopper();
}
_loadEvent() {
this.resizeAnimationStopper();
}
resizeAnimationStopper() {
this.classList.add("resize-animation-stopper");
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
this.classList.remove("resize-animation-stopper");
}, 500);
}
}
customElements.define("iga-tab", IgaTab);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.