• HTML

    tab-list.html html

    <tab-list>
    	<menu>
    		<li><button type="button" aria-pressed="true">Tab 1</button></li>
    		<li><button type="button">Tab 2</button></li>
    		<li><button type="button">Tab 3</button></li>
    	</menu>
    	<details open>
    		<summary>Tab 1</summary>
    		Tab 1 content
    	</details>
    	<details>
    		<summary>Tab 2</summary>
    		Tab 2 content
    	</details>
    	<details>
    		<summary>Tab 3</summary>
    		Tab 3 content
    	</details>
    </tab-list>
    CSS

    tab-list.css css

    tab-list {
    	display: block;
    	margin-bottom: var(--space-l);
    
    	> menu {
    		display: flex;
        	border-bottom: 1px solid var(--color-gray-50);
    		padding: 0;
        	margin-bottom: 0;
    
    		> li {
    			display: inline-block;
    			margin: 0;
    			padding: 0;
    		}
    
    		& button {
    			border: 0;
    			border-top: 2px solid transparent;
          		border-bottom-width: 0;
          		font-family: var(--font-family-sans);
    			font-size: var(--font-size-s);
          		font-weight: var(--font-weight-bold);
    			padding: var(--space-s) var(--space-m);
    			color: var(--color-text-soft);
    			background-color: var(--color-secondary);
          		cursor: pointer;
          		transition: all var(--transition-short) var(--easing-inout);
    
    			&:hover,
    			&:focus {
    				color: var(--color-text);
    				background-color: var(--color-secondary-hover);
    			}
          
    			&:active {
    				color: var(--color-text);
    				background-color: var(--color-secondary-active);
    			}
    
    			&[aria-pressed="true"] {
            		color: var(--color-primary-active);
    				border-top: 3px solid var(--color-primary);
    				background-color: var(--color-background);
            		margin-bottom: -1px;
    			}
    		}
    	}
      
    	> details {
    		font-family: sans-serif;
    		font-size: var(--font-size-m);
    		background: var(--color-background);
    		
    		& summary {
    			cursor: pointer;
    			font-size: var(--font-size-m);
    			font-weight: var(--font-weight-bold);
    			margin: 0 0 var(--space-s);
    		}
    
    		::marker,
    		::-webkit-details-marker {
    			color: var(--color-text-soft);
    		}
    
    		&[open] {
    			padding-block: var(--space-m);
    		}
      	}
      
    	&[accordion] {
    		
    		> menu {
    			display: none;
    		}
    		
    		details[open] {
    			padding-top: 0;
    		}
    	}
    	
    	&:not([accordion]) > details summary {
    		display: none;
    	}
    }
    TS

    tab-list.ts ts

    import { UIElement, asBoolean, setAttribute, setProperty, toggleAttribute, toggleClass } from '../../../'
    
    export class TabList extends UIElement<{
    	active: number,
        accordion: boolean,
    }> {
    	static readonly localName = 'tab-list'
    	static observedAttributes = ['accordion']
    
    	init = {
    		active: 0,
    		accordion: asBoolean,
    	}
    
    	connectedCallback() {
    		super.connectedCallback()
    
    		// Set inital active tab by querying details[open]
    		const getInitialActive = () => { 
    			const panels = Array.from(this.querySelectorAll('details'))
    			for (let i = 0; i < panels.length; i++) {
    				if (panels[i].hasAttribute('open')) return i
    			}
    			return 0
    		}
    		this.set('active', getInitialActive())
    
    		// Reflect accordion attribute (may be used for styling)
    		this.self.sync(toggleAttribute('accordion'))
    
    		// Update active tab state and bind click handlers
    		this.all('menu button')
    			.on('click', (_, index) => () => {
    				this.set('active', index)
    			})
    			.sync(setProperty(
    				'ariaPressed',
    				(_, index) => String(this.get('active') === index)
    			))
    
    		// Update details panels open, hidden and disabled states
    		this.all<HTMLDetailsElement>('details').sync(
    			setProperty(
    				'open',
    				(_, index) => !!(this.get('active') === index)
    			),
    			setAttribute(
    				'aria-disabled',
    				() => String(!this.get('accordion'))
    			)
    		)
    
    		// Update summary visibility
    		this.all('summary').sync(toggleClass(
    			'visually-hidden',
    			() => !this.get('accordion')
    		))
    	}
    }
    TabList.define()