Accessible Tabs

by ninty9notout

HTML

<h2 id="heading">Operating Systems</h2>

<div class="tablist" role="tablist" aria-labelledby="heading">
    <label class="tab">
        <input type="radio" class="radio" name="tab" value="iOS" role="tab" aria-selected="true" aria-controls="tabpanel-1" checked>
        <span>iOS</span>
    </label>
    <label class="tab">
        <input type="radio" class="radio" name="tab" value="Android" role="tab" aria-selected="false" aria-controls="tabpanel-2">
        <span>Android</span>
    </label>
    <label class="tab">
        <input type="radio" class="radio" name="tab" value="HarmonyOS" role="tab" aria-selected="false" aria-controls="tabpanel-3">
        <span>HarmonyOS</span>
    </label>
    <label class="tab">
        <input type="radio" class="radio" name="tab" value="KaiOS" role="tab" aria-selected="false" aria-controls="tabpanel-4">
        <span>KaiOS</span>
    </label>
</div>

<div class="tabpanel is-selected" id="tabpanel-1" role="tabpanel">
    <ul>
        <li>Developed by Apple, iOS is exclusive to Apple's devices like the iPhone and iPad.</li>
        <li>Known for its sleek design, privacy focus, and seamless integration with the Apple ecosystem.</li>
        <li>iOS updates are centralized, making them available to all Apple users simultaneously.</li>
    </ul>
</div>

<div class="tabpanel" id="tabpanel-2" role="tabpanel">
    <ul>
        <li>Developed by Google, Android is the most widely used smartphone OS globally.</li>
        <li>It is open-source and allows manufacturers to customize the software for their devices.</li>
        <li>Major brands using Android include Samsung, Google, Xiaomi, Oppo, and Huawei.</li>
    </ul>
</div>

<div class="tabpanel" id="tabpanel-3" role="tabpanel">
    <ul>
        <li>Developed by Huawei, HarmonyOS is a relatively new operating system, designed as a multi-device platform.</li>
        <li>Initially launched for smart TVs and other IoT devices, it has started appearing on Huawei smartphones due to...

SCSS

@mixin visually-hidden() {
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;

    &:not(caption) {
        position: absolute;
    }
}

:focus-visible {
    outline: 2px solid #f0f;
    outline-offset: 2px;
}

.tab {
    display: inline-block;
    padding: 10px 20px;
    z-index: 1;
    border-bottom: 1px solid transparent;
    margin-bottom: -1px;
    
    &:hover {
        border-bottom-color: rgba(255, 255, 255, 1);
    }
    
    &:has(input:checked) {                    
        border-bottom-color: rgba(255, 0, 0, 1);
    }
}

.radio {
    @include visually-hidden();
}

.tabpanel {
    padding-top: 10px;
    border-top: 1px solid rgba(255, 255, 255, .5);

    &:not(.is-selected) {
        display: none;
    }
}

JavaScript

document.querySelectorAll('.tab input').forEach(input => {
	input.addEventListener('change', event => {
    	// Hide previously selected tab panel
    	document.querySelector('.is-selected').classList.remove('is-selected');
        // Switch aria selected for previously selected tab
        document.querySelector('[aria-selected="true"]').setAttribute('aria-selected', 'false');
        
        // Grab current tab
        const tab = event.currentTarget;
        const tabpanel = tab.getAttribute('aria-controls');

        // Set current active
        tab.setAttribute('aria-selected', 'true');
        document.getElementById(tabpanel).classList.add('is-selected');
    });
});