Navegación
Estado: BetaPestañas animadas
Para secciones hermanas de una vista (horario, precios, maestras): la pastilla de fondo mide la pestaña elegida y viaja hasta ella con resorte, y el panel cambia con un fundido corto mientras la caja ajusta su altura. Patrón ARIA tabs completo, con flechas, Inicio y Fin.
- Resorte
- ARIA tabs
- Teclado
- Lun 7:00 Vinyasa Ana
- Mié 19:00 Reformer Sofía
- Sáb 9:00 Barre Lucía
- Clase suelta $180
- Paquete de 10 $1,450
Los créditos de paquete duran 30 días.
- Ana Ruiz Vinyasa y meditación
- Sofía Méndez Reformer
- Lucía Cano Barre
- Paula Vega Yin y restaurativa
- Lun 7:00 Vinyasa Ana
- Mié 19:00 Reformer Sofía
- Sáb 9:00 Barre Lucía
- Clase suelta $180
- Paquete de 10 $1,450
Los créditos de paquete duran 30 días.
- Ana Ruiz Vinyasa y meditación
- Sofía Méndez Reformer
- Lucía Cano Barre
- Paula Vega Yin y restaurativa
app/views/… (la demo de arriba)
<div class="w-full max-w-sm">
<%= render Ui::AnimatedTabsComponent.new(label: "Información del estudio", id: "animated-tabs-demo") do |tabs| %>
<% tabs.with_tab(label: "Horario", id: "horario") do %>
<ul class="divide-y divide-border-subtle rounded-xl border border-border bg-card">
<% [ [ "Lun 7:00", "Vinyasa", "Ana" ], [ "Mié 19:00", "Reformer", "Sofía" ], [ "Sáb 9:00", "Barre", "Lucía" ] ].each do |time, name, teacher| %>
<li class="flex items-center gap-3 px-3.5 py-2.5">
<span class="w-16 shrink-0 font-mono text-xs text-faint"><%= time %></span>
<span class="min-w-0 flex-1 truncate text-foreground"><%= name %></span>
<span class="shrink-0 text-xs"><%= teacher %></span>
</li>
<% end %>
</ul>
<% end %>
<% tabs.with_tab(label: "Precios", id: "precios") do %>
<ul class="divide-y divide-border-subtle rounded-xl border border-border bg-card">
<% [ [ "Clase suelta", "$180" ], [ "Paquete de 10", "$1,450" ] ].each do |name, price| %>
<li class="flex items-center justify-between gap-3 px-3.5 py-2.5">
<span class="text-foreground"><%= name %></span>
<span class="font-mono text-xs text-foreground"><%= price %></span>
</li>
<% end %>
</ul>
<p class="mt-2.5 px-1 text-xs">Los créditos de paquete duran 30 días.</p>
<% end %>
<% tabs.with_tab(label: "Maestras", id: "maestras") do %>
<ul class="space-y-2.5 px-1">
<% [ [ "AR", "Ana Ruiz", "Vinyasa y meditación" ], [ "SM", "Sofía Méndez", "Reformer" ], [ "LC", "Lucía Cano", "Barre" ], [ "PV", "Paula Vega", "Yin y restaurativa" ] ].each do |initials, name, focus| %>
<li class="flex items-center gap-3">
<span class="grid size-8 shrink-0 place-items-center rounded-full border border-border bg-raised font-mono text-2xs text-muted-foreground" aria-hidden="true"><%= initials %></span>
<span class="min-w-0 leading-tight">
<span class="block truncate text-foreground"><%= name %></span>
<span class="block truncate text-xs"><%= focus %></span>
</span>
</li>
<% end %>
</ul>
<% end %>
<% end %>
</div>Qué hace
- La pastilla viaja con el resorte "morph" en x y ancho, medidos sobre la pestaña real.
- El panel que sale se desvanece encima del que entra; la altura cambia con "gentle".
- Dispara animated-tabs:change con { id, index } para cargar contenido o guardar la pestaña.
- Si cambian las fuentes o el ancho, la pastilla se reacomoda sin animar.
Props
| Nombre | Tipo | Default | Descripción |
|---|---|---|---|
| label | String | — | Nombre accesible de la lista de pestañas (aria-label). |
| selected | String | nil | id de la pestaña activa al cargar; si no, la primera. |
| shape | :segment | :pill | :segment | :pill redondea lista y pastilla por completo (variant: es su nombre anterior). |
| id | String | aleatorio | Prefijo de los ids de pestañas y paneles. |
| with_tab(label:, id:, badge: nil) { } | Slot | — | Una pestaña; el bloque es el contenido de su panel. |
Accesibilidad
- role tablist / tab / tabpanel con aria-selected, aria-controls y aria-labelledby.
- ←/→ recorren con vuelta, Inicio y Fin saltan; la selección sigue al foco.
- Tabindex itinerante; el panel es enfocable para llegar al contenido con Tab.
- Sin JS se ve la pestaña activa marcada y solo su panel, sin brincos al cargar.