Tabs sem JavaScript

Tabs (ou abas) são um componente comum em interfaces. Elas permitem que o usuário navegue entre diferentes seções de conteúdo sem precisar recarregar a página. Por padrão, no HTML, não existe um elemento específico para criar abas. Portanto, é comum vermos abas sendo implementadas com JavaScript, para controlar o conteúdo a ser exibido e, claro, CSS para estilizar.


Mas e se eu te disser que é possível criar abas sem JavaScript? Neste artigo, vamos ver como fazer isso e ainda adicionar uma animação muito interessante.


Ao final desse artigo, você chegará a esse resultado:


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3


O básico

O princípio é: precisamos conseguir alternar a visibilidade de diferentes seções. Como não utilizaremos JavaScript, precisamos de uma forma para definir o estado de cada aba. Para isso, vamos utilizar inputs do tipo radio.


O que faz o radio ser uma boa escolha é, além da possibilidade de verificar pelo CSS se está selecionado, a garantia de que apenas um deles pode estar selecionado por vez.


O HTML então pode ser estruturado dessa forma:


<nav>
    <ul>
        <li>
            <label>
                <input type="radio" name="tab" checked />
                <span>Tab 1</span>
            </label>
        </li>
        <li>
            <label>
                <input type="radio" name="tab" />
                <span>Tab 2</span>
            </label>
        </li>
        <li>
            <label>
                <input type="radio" name="tab" />
                <span>Tab 3</span>
            </label>
        </li>
    </ul>
</nav>

Temos então uma navegação com três abas. Cada aba é representada por um label que contém um input do tipo radio. O atributo name dos inputs deve ser o mesmo para que eles se comportem como um grupo.


Conteúdo

Agora que temos a navegação, precisamos exibir o conteúdo de cada aba. Para isso, vamos utilizar o seletor :checked do CSS. Isso nos permite estilizar elementos que estão selecionados ou, como você verá adiante, verificar se um elemento pai contém um elemento filho que está selecionado.


Primeiro vamos criar o conteúdo de cada aba:


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3

<nav>
    <ul>
        <li>
            <label>
                <input type="radio" name="tab" checked />
                <span>Tab 1</span>
            </label>
        </li>
        <li>
            <label>
                <input type="radio" name="tab" />
                <span>Tab 2</span>
            </label>
        </li>
        <li>
            <label>
                <input type="radio" name="tab" />
                <span>Tab 3</span>
            </label>
        </li>
    </ul>
</nav>
<div style="position: relative;">
    <section class="tab-content">
        <h3>Tab 1</h3>
        <p>Conteúdo da aba 1</p>
    </section>
    <section class="tab-content">
        <h3>Tab 2</h3>
        <p>Conteúdo da aba 2</p>
    </section>
    <section class="tab-content">
        <h3>Tab 3</h3>
        <p>Conteúdo da aba 3</p>
    </section>
</div>

Agora é preciso esconder todo o conteúdo e exibir apenas o conteúdo da aba que está selecionada.


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3

.tab-content {
    display: none;
}
        
nav:has(li:nth-of-type(1) input:checked) ~ div .tab-content:nth-of-type(1),
nav:has(li:nth-of-type(2) input:checked) ~ div .tab-content:nth-of-type(2),
nav:has(li:nth-of-type(3) input:checked) ~ div .tab-content:nth-of-type(3) {
    display: block;
}

Perceba que aqui estou utilizando a ordenação dos elementos como critério para exibir o conteúdo. Isso é feito com o seletor nth-of-type. O seletor :has é utilizado para verificar se um elemento pai contém um elemento filho que corresponde a um seletor. No caso, estamos verificando se o nav contém um input que está selecionado (checked). Se sim, exibimos o conteúdo da aba correspondente.


Aqui por si só já temos um sistema de abas funcional sem JavaScript.


Estilização

Para começar a estilização, podemos deixar as abas com a aparência de botões. Nesse caso, vou esconder os inputs e estilizar os labels.


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3

.tab-content {
    ...
    margin-top: 1rem;
}

...

nav ul li input {
    display: none;
}

nav ul li {
    display: inline-block;
}

nav ul li label {
    cursor: pointer;
    padding: 0.5rem 1rem;
    border-radius: 99px;
}

nav ul li label:has(input:checked) {
    background-color: #22272e;
    color: white;
}

Aqui estou escondendo os inputs e estilizando os labels para que pareçam botões. Também adicionei um pouco de espaçamento entre o conteúdo das abas.


Um pouco de animação pode deixar o componente mais interessante. Vamos adicionar uma transição ao conteúdo das abas e ao fundo dos botões.


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3

.tab-content {
    ...
    opacity: 0;
    visibility: hidden;
    position: absolute;
    top: 0;
    transform: translateY(-1rem);
    transition: transform 0.25s, opacity 0.5s;
}

nav:has(li:nth-of-type(1) input:checked) ~ div .tab-content:nth-of-type(1),
nav:has(li:nth-of-type(2) input:checked) ~ div .tab-content:nth-of-type(2),
nav:has(li:nth-of-type(3) input:checked) ~ div .tab-content:nth-of-type(3) {
    opacity: 1;
    position: relative;
    visibility: visible;
    transform: translateY(0);
}

...

nav ul li label {
    ...
    transition: 0.25s;
}

...

Agora o conteúdo das abas está escondido com opacity e visibility. Quando uma aba é selecionada, o conteúdo é exibido com uma transição suave. Os botões também possuem uma transição no fundo.


Aqui já temos um resultado muito interessante. Mas eu vou além utilizando uma animação de slide do fundo dos botões. Nosso melhor amigo aqui será o clip-path. Com o clip-path podemos definir uma máscara para um elemento. O que eu vou fazer é duplicar a navegação deixando a cor de fundo dos botões e sobrepô-la aos botões originais. A máscara será um retângulo que vai se mover de acordo com a aba selecionada.


Primeiro, envolvo a navegação em um elemento pai com id “nav” e a duplico. A navegação duplicada leva o atributo aria-hidden=“true” para que ela não seja acessível. Seus inputs também são desabilitados.


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3

<div id="nav" style="position: relative;">
    <nav>
        <ul>
            <li>
                <label>
                    <input type="radio" name="tab" checked />
                    <span>Tab 1</span>
                </label>
            </li>
            <li>
                <label>
                    <input type="radio" name="tab" />
                    <span>Tab 2</span>
                </label>
            </li>
            <li>
                <label>
                    <input type="radio" name="tab" />
                    <span>Tab 3</span>
                </label>
            </li>
        </ul>
    </nav>
    <nav aria-hidden="true">
        <ul>
            <li>
                <label>
                    <input type="radio" disabled="true" />
                    <span>Tab 1</span>
                </label>
            </li>
            <li>
                <label>
                    <input type="radio" disabled="true" />
                    <span>Tab 2</span>
                </label>
            </li>
            <li>
                <label>
                    <input type="radio" disabled="true" />
                    <span>Tab 3</span>
                </label>
            </li>
        </ul>
    </nav>
</div>
...

Agora a navegação duplicada recebe o fundo dos botões. Repare nas outras mudanças como o padding e border-radius que foram removidos dos labels e adicionados ao nav pai. Perceba também que o conteúdo deixou de ser exibido. Isso acontece porque o seletor que controla a exibição do conteúdo das abas considera que a navegação está no mesmo nível. Para corrigir isso, substituiremos nav no seletor por #nav, que é o id do elemento pai.


Aplicando o clip-path com o transition temos o efeito desejado.


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3

nav {
    padding: 0.5rem 0;
}

nav[aria-hidden="true"] {
    position: absolute;
    top: 0;
    left: 0;
    background-color: #22272e;
    color: white;
    z-index: 1;
    pointer-events: none;
    transition: 0.25s cubic-bezier(.5,.01,0,.97);
}

nav:has(li:nth-of-type(1) input:checked) ~ nav[aria-hidden="true"] {
    clip-path: inset(0 66% 0 0 round 99px);
}

nav:has(li:nth-of-type(2) input:checked) ~ nav[aria-hidden="true"] {
    clip-path: inset(0 33% 0 33% round 99px);
}

nav:has(li:nth-of-type(3) input:checked) ~ nav[aria-hidden="true"] {
    clip-path: inset(0 0 0 66% round 99px);
}

#nav:has(li:nth-of-type(1) input:checked) ~ div .tab-content:nth-of-type(1),
#nav:has(li:nth-of-type(2) input:checked) ~ div .tab-content:nth-of-type(2),
#nav:has(li:nth-of-type(3) input:checked) ~ div .tab-content:nth-of-type(3) {
    ...
}

...

nav ul li label {
    ...
    padding: 0 1rem;
}

Como falei, o clip-path é utilizado para criar uma máscara. O valor inset define uma máscara retangular. A ordem dos valores é top right bottom left. O que eu considero aqui é que tenho três botões, então a máscara deve revelar um terço da largura da navegação. Para o primeiro botão, defino todos os valores como zero, exceto o right que é 66%. Isso faz com que a máscara inicie do 0 em todas as direções, mas a direita ela inicia em 66% da largura, que corresponde a dois terços da largura total. Para o segundo botão, a máscara inicia em 33% (à esquerda) e termina em 33% (à direita). Por fim, para o terceiro botão, a máscara inicia em 66% (à esquerda) e termina em 0 (à direita).


O round é utilizado para arredondar as bordas da máscara. O valor 99px, que se refere ao arrerondamento, é o valor que estava sendo utilizado anteriormente no border-radius.


Com o transition temos um efeito suave na transição da máscara. Utilizei um valor de cubic-bezier para deixar a transição mais suave.


Deixo abaixo o mesmo resultado 20x mais lento para que você possa ver o funcionamento do efeito.


Tab 1

Conteúdo da aba 1

Tab 2

Conteúdo da aba 2

Tab 3

Conteúdo da aba 3


Quando a View Transitions API atingir um nível de suporte mais amplo, será possível fazer essas animações sem a necessidade de duplicar a navegação, com bem menos esforço.