Componentes
Tabs
Tablist + panels com troca automática de conteúdo. Acessível via teclado (Arrow/Home/End) e ARIA completo.
Default
Cada slot nomeado com o id da tab vira o painel correspondente.
Conteúdo da visão geral.
Conteúdo de detalhes.
Histórico do paciente.
<Tabs tabs={[
{ id: "overview", label: "Visão geral" },
{ id: "details", label: "Detalhes" },
{ id: "history", label: "Histórico" },
]}>
<Fragment slot="overview">Conteúdo da visão geral.</Fragment>
<Fragment slot="details">Conteúdo de detalhes.</Fragment>
<Fragment slot="history">Histórico do paciente.</Fragment>
</Tabs>
<Tabs tabs={[
{ id: "overview", label: "Visão geral" },
{ id: "details", label: "Detalhes" },
{ id: "history", label: "Histórico" },
]}>
<Fragment slot="overview">Conteúdo da visão geral.</Fragment>
<Fragment slot="details">Conteúdo de detalhes.</Fragment>
<Fragment slot="history">Histórico do paciente.</Fragment>
</Tabs> <template>
<div>
<div role="tablist" class="flex border-b border-border">
<button
v-for="t in tabs"
:key="t.id"
role="tab"
:aria-selected="active === t.id"
:class="['px-4 py-2 text-sm font-medium border-b-2 transition-colors',
active === t.id ? 'text-foreground border-primary' : 'text-muted-foreground border-transparent hover:text-foreground']"
@click="active = t.id"
>{{ t.label }}</button>
</div>
<div role="tabpanel" class="p-4 text-sm">
<p v-if="active === 'overview'">Conteúdo da visão geral.</p>
<p v-else-if="active === 'details'">Conteúdo de detalhes.</p>
<p v-else>Histórico do paciente.</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tabs = [
{ id: 'overview', label: 'Visão geral' },
{ id: 'details', label: 'Detalhes' },
{ id: 'history', label: 'Histórico' },
]
const active = ref('overview')
</script>
<template>
<div>
<div role="tablist" class="flex border-b border-border">
<button
v-for="t in tabs"
:key="t.id"
role="tab"
:aria-selected="active === t.id"
:class="['px-4 py-2 text-sm font-medium border-b-2 transition-colors',
active === t.id ? 'text-foreground border-primary' : 'text-muted-foreground border-transparent hover:text-foreground']"
@click="active = t.id"
>{{ t.label }}</button>
</div>
<div role="tabpanel" class="p-4 text-sm">
<p v-if="active === 'overview'">Conteúdo da visão geral.</p>
<p v-else-if="active === 'details'">Conteúdo de detalhes.</p>
<p v-else>Histórico do paciente.</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tabs = [
{ id: 'overview', label: 'Visão geral' },
{ id: 'details', label: 'Detalhes' },
{ id: 'history', label: 'Histórico' },
]
const active = ref('overview')
</script> import { useState } from 'react';
const tabs = [
{ id: 'overview', label: 'Visão geral', body: 'Conteúdo da visão geral.' },
{ id: 'details', label: 'Detalhes', body: 'Conteúdo de detalhes.' },
{ id: 'history', label: 'Histórico', body: 'Histórico do paciente.' },
];
export function PatientTabs() {
const [active, setActive] = useState('overview');
const current = tabs.find((t) => t.id === active);
return (
<div>
<div role="tablist" className="flex border-b border-border">
{tabs.map((t) => (
<button
key={t.id}
role="tab"
aria-selected={active === t.id}
onClick={() => setActive(t.id)}
className={[
'px-4 py-2 text-sm font-medium border-b-2 transition-colors',
active === t.id ? 'text-foreground border-primary' : 'text-muted-foreground border-transparent hover:text-foreground',
].join(' ')}
>{t.label}</button>
))}
</div>
<div role="tabpanel" className="p-4 text-sm">{current?.body}</div>
</div>
);
}
import { useState } from 'react';
const tabs = [
{ id: 'overview', label: 'Visão geral', body: 'Conteúdo da visão geral.' },
{ id: 'details', label: 'Detalhes', body: 'Conteúdo de detalhes.' },
{ id: 'history', label: 'Histórico', body: 'Histórico do paciente.' },
];
export function PatientTabs() {
const [active, setActive] = useState('overview');
const current = tabs.find((t) => t.id === active);
return (
<div>
<div role="tablist" className="flex border-b border-border">
{tabs.map((t) => (
<button
key={t.id}
role="tab"
aria-selected={active === t.id}
onClick={() => setActive(t.id)}
className={[
'px-4 py-2 text-sm font-medium border-b-2 transition-colors',
active === t.id ? 'text-foreground border-primary' : 'text-muted-foreground border-transparent hover:text-foreground',
].join(' ')}
>{t.label}</button>
))}
</div>
<div role="tabpanel" className="p-4 text-sm">{current?.body}</div>
</div>
);
} <!-- Preline tabs: data-hs-tab + data-hs-tab-select -->
<div>
<nav role="tablist" class="flex border-b border-border">
<button type="button" data-hs-tab="#panel-1" class="px-4 py-2 text-sm font-medium border-b-2 text-foreground border-primary" role="tab">Visão geral</button>
<button type="button" data-hs-tab="#panel-2" class="px-4 py-2 text-sm font-medium border-b-2 text-muted-foreground border-transparent hover:text-foreground" role="tab">Detalhes</button>
<button type="button" data-hs-tab="#panel-3" class="px-4 py-2 text-sm font-medium border-b-2 text-muted-foreground border-transparent hover:text-foreground" role="tab">Histórico</button>
</nav>
<div id="panel-1" role="tabpanel" class="p-4 text-sm">Conteúdo da visão geral.</div>
<div id="panel-2" role="tabpanel" class="p-4 text-sm hidden">Conteúdo de detalhes.</div>
<div id="panel-3" role="tabpanel" class="p-4 text-sm hidden">Histórico do paciente.</div>
</div>
<!-- Preline tabs: data-hs-tab + data-hs-tab-select -->
<div>
<nav role="tablist" class="flex border-b border-border">
<button type="button" data-hs-tab="#panel-1" class="px-4 py-2 text-sm font-medium border-b-2 text-foreground border-primary" role="tab">Visão geral</button>
<button type="button" data-hs-tab="#panel-2" class="px-4 py-2 text-sm font-medium border-b-2 text-muted-foreground border-transparent hover:text-foreground" role="tab">Detalhes</button>
<button type="button" data-hs-tab="#panel-3" class="px-4 py-2 text-sm font-medium border-b-2 text-muted-foreground border-transparent hover:text-foreground" role="tab">Histórico</button>
</nav>
<div id="panel-1" role="tabpanel" class="p-4 text-sm">Conteúdo da visão geral.</div>
<div id="panel-2" role="tabpanel" class="p-4 text-sm hidden">Conteúdo de detalhes.</div>
<div id="panel-3" role="tabpanel" class="p-4 text-sm hidden">Histórico do paciente.</div>
</div> Com ícones
icon no array de tabs — Material Symbol antes do label.
Resumo executivo.
Lista de arquivos.
Membros do time.
<Tabs tabs={[
{ id: "summary", label: "Resumo", icon: "description" },
{ id: "files", label: "Arquivos", icon: "folder" },
{ id: "team", label: "Equipe", icon: "group" },
]}>
<Fragment slot="summary">Resumo executivo.</Fragment>
<Fragment slot="files">Lista de arquivos.</Fragment>
<Fragment slot="team">Membros do time.</Fragment>
</Tabs>
<Tabs tabs={[
{ id: "summary", label: "Resumo", icon: "description" },
{ id: "files", label: "Arquivos", icon: "folder" },
{ id: "team", label: "Equipe", icon: "group" },
]}>
<Fragment slot="summary">Resumo executivo.</Fragment>
<Fragment slot="files">Lista de arquivos.</Fragment>
<Fragment slot="team">Membros do time.</Fragment>
</Tabs> Eventos
Componente dispara tabs:change com detail.tab. Escute para sincronizar URL ou refetch.
<Tabs tabsId="patient-view" tabs={[...]}>...</Tabs>
<script>
document
.querySelector('[data-tabs-id="patient-view"]')
?.addEventListener('tabs:change', (e) => {
const { tab } = (e as CustomEvent<{ tab: string }>).detail;
history.replaceState(null, '', `?tab=${tab}`);
});
</script>
Acessibilidade
role="tablist"no container,role="tab"em cada botão,role="tabpanel"em cada painelaria-selected,aria-controls,aria-labelledbyligados via id- Setas ←/→ navegam, Home/End vão para extremos
- Painel ativo é
tabindex="0"para receber foco direto
Props
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
tabs | Tab[] | obrigatório | Lista de tabs. |
activeTab | string | primeira | ID inicial. |
tabsId | string | auto | ID estável para event delegation. |
class | string | — | Classes adicionais. |
Tab
| Prop | Tipo | Descrição |
|---|---|---|
id | string | Identificador único. Usado como nome do slot. |
label | string | Texto da tab. |
icon | string | Material Symbol opcional. |