Control segmentado

Para los filtros del admin (Todos / Alumnos / Maestros / Admins) y cualquier grupo corto de opciones excluyentes. La pastilla viaja con resorte hasta la opción activa. Enlaces para filtros del servidor, radios nativos para un valor de formulario o del cliente, o pestañas ARIA para paneles.

app/views/… (la demo de arriba)

<div class="@container flex w-full max-w-md flex-col items-center gap-5">
  <%# Filtros del servidor: enlaces con aria-current. En tu app: href: people_path(role: "client"),
      sin turbo: false (aquí evita que la demo recargue la página). %>
  <%= render Ui::SegmentedControlComponent.new(label: "Filtrar por rol", selected: "all") do |seg| %>
    <% seg.with_option(label: "Todos", value: "all", count: 47, href: "#segmented-todos", data: { turbo: false }) %>
    <% seg.with_option(label: "Alumnos", value: "student", count: 38, href: "#segmented-alumnos", data: { turbo: false }) %>
    <% seg.with_option(label: "Maestros", value: "teacher", count: 6, href: "#segmented-maestros", data: { turbo: false }) %>
    <% seg.with_option(label: "Admins", value: "admin", count: 3, href: "#segmented-admins", data: { turbo: false }) %>
  <% end %>

  <%# Un valor para un formulario: radios nativos que se mandan con name:. %>
  <%= render Ui::SegmentedControlComponent.new(label: "Periodo", name: "range", selected: "week", shape: :pill, variant: :solid) do |seg| %>
    <% seg.with_option(label: "Hoy", value: "today") %>
    <% seg.with_option(label: "Semana", value: "week") %>
    <% seg.with_option(label: "Mes", value: "month") %>
    <% seg.with_option(label: "Año", value: "year") %>
  <% end %>

  <%# Paneles de la misma vista: patrón ARIA tabs. %>
  <div class="hidden w-full flex-col items-center gap-3 @sm:flex">
    <%= render Ui::SegmentedControlComponent.new(label: "Clases", mode: :tabs, selected: "proximas", size: :md) do |seg| %>
      <% seg.with_option(label: "Próximas", value: "proximas", count: 12, controls: "segmented-panel-proximas") %>
      <% seg.with_option(label: "Pasadas", value: "pasadas", controls: "segmented-panel-pasadas") %>
    <% end %>
    <p id="segmented-panel-proximas" role="tabpanel" class="text-xs text-faint">12 clases en los próximos 7 días · la siguiente, Reformer 7:00</p>
    <p id="segmented-panel-pasadas" role="tabpanel" class="text-xs text-faint" hidden>148 clases este mes · 91% de asistencia</p>
  </div>
</div>

Qué hace

  • Pastilla con el resorte "morph" (posición y ancho por separado); sigue a la opción si cambia el ancho.
  • En enlaces la pastilla se adelanta al clic mientras Turbo trae la página filtrada.
  • Si no cabe, se desplaza sola hasta la activa y desvanece la orilla que tiene más.
  • Dispara segmented-control:change con { value, index } en los tres modos.
  • Sin JS la opción activa se marca sola con el mismo fondo.

Props

Nombre Tipo Default Descripción
label String — Nombre accesible del grupo.
with_option(label:, value:, count:, href:, controls:) slot — Una opción con su conteo; href la vuelve enlace, controls apunta a su panel (modo tabs).
selected String la primera El value de la opción activa (p. ej. params[:role]).
mode :links | :radio | :tabs :links con href, si no :radio Quién decide: el servidor, un formulario/el cliente o paneles de la vista.
name String nil Modo radio: el nombre con el que se manda el valor.
form / attribute FormBuilder / Symbol nil Modo radio: name y selected salen del modelo.
size / shape / variant :sm|:md · :segment|:pill · :soft|:solid :sm · :segment · :soft Alto, forma (sigue el dial de redondez) y si la pastilla es una capa elevada o el acento lleno. tone: :subtle|:solid es el nombre anterior de variant:.
full_width Boolean false Las opciones se reparten el ancho.

Accesibilidad