Fundamentos
Íconos
Un solo set, un solo trazo y tamaños fijos.
Un solo set para toda la interfaz: 89 íconos en
6 grupos, dibujados en una rejilla de 24 con un trazo de
2, puntas y uniones redondas, sin rellenos, y del color del texto que
los rodea (currentColor). Los pinta Ui::IconComponent; la geometría vive en
app/components/ui/icons.rb y viene de Lucide
(licencia ISC).
El set
Busca por el nombre en inglés (como en Lucide) o por lo que buscas en español: «tarjeta», «maestra», «eliminar». Un clic o Enter copia el nombre listo para Ruby.
89 íconos
Flechas y navegación
Acciones
Estado y avisos
Agenda y personas
Comercio
Contenido y contacto
Ningún ícono coincide. Prueba en inglés o con otra palabra; si de verdad falta, agrégalo al set.
Tamaños y trazo
Cinco tamaños fijos. El ícono acompaña al texto de su tamaño: chico junto a metadatos,
md en botones, menús y campos, lg en cuadritos y avisos, xl
solo en marcas grandes (estados vacíos, la foto de perfil vacía). Nada de size-* sueltos
ni size-[18px]: si ninguno cabe, el diseño es el que no cabe.
:xs — trazo —
:sm — trazo —
:md — trazo —
:lg — trazo —
:xl — trazo —
xs · con text-2xs
sm · mar 14 oct · 07:00
lg · en un cuadrito de 40px
| Tamaño | px | Trazo en pantalla | Va con | Usos hoy |
|---|---|---|---|---|
:xs |
12 | 1px | text-2xs y text-3xs: pastillas, flechas de tendencia, migas | 8 |
:sm |
14 | 1.17px | text-xs y text-compact: metadatos, errores de campo, botones chicos | 36 |
:md (default) |
16 | 1.33px | text-sm: botones, menús, campos, cerrar | 30 |
:lg |
20 | 1.67px | cuadritos de 32–40px, la campana, diálogos | 6 |
:xl |
24 | 2px | marcas grandes: estado vacío, foto vacía | 1 |
Por qué el trazo es 2
A 14px un trazo de 2 queda en ~1.17px: el mismo grosor que el asta de Inter a 13–14px, así que
ícono y etiqueta pesan igual. Con 1.75 baja a ~1px, cae entre dos columnas de píxeles y en los
tonos tenues (text-subtle) se ve gris y borroso, sobre todo en oscuro. Además la rejilla
de Lucide está dibujada para 2: los puntos de la alerta o del pin salen del grosor. Un solo
trazo para todo; por eso Ui::IconComponent no acepta stroke_width:.
Color
El ícono no tiene color propio: toma el del texto (currentColor). Se le pone una clase
de texto solo cuando debe ir más tenue o llevar un estado.
text-subtlejunto a metadatos (hora, lugar) y dentro de campos.text-muted-foregrounden menús y botones de ícono en reposo;text-foregroundal pasar el cursor.text-warning,text-destructive,text-successsolo cuando el ícono lleva un estado, y siempre con texto al lado.
Vinyasa Flow
07:00–08:00
Sala Luna
Editar
Duplicar
Eliminar
Faltan menos de 12 horas: el crédito no se devuelve.
Decorativo o con nombre
Casi siempre el ícono es decorativo: el texto de al lado o el aria-label del botón ya
dicen qué es, y el lector de pantalla no debe leerlo dos veces. Sin label: sale con
aria-hidden="true".
- Ícono con texto (Editar, Reservar clase): decorativo.
- Botón solo de ícono (cerrar, más acciones): el nombre va en el botón (
aria-label="Cerrar aviso") y el ícono sigue decorativo. - El ícono es la información (una palomita que dice «reservada» sin texto, un estado en una celda):
label:, y sale conrole="img"y suaria-label.
Lo que pinta cada uno
# Ui::IconComponent.svg(:x, size: :sm)
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-3.5 shrink-0" data-icon="x" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
# Ui::IconComponent.svg(:check, size: :xs, label: "Reservada")
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-3 shrink-0" data-icon="check" role="img" aria-label="Reservada"><path d="M20 6 9 17l-5-5"/></svg>
Cómo se usa
En una plantilla, con render. En un método call o en un helper (Ruby, sin
plantilla), con el atajo Ui::IconComponent.svg, que devuelve el HTML. Los nombres
aceptan guion o guion bajo: :chevron_down y "chevron-down" son el mismo.
Un nombre que no existe falla en desarrollo y en las pruebas, con sugerencia.
Plantilla (.html.erb) y Ruby
<%# Junto a un texto: decorativo, toma el color del texto. %>
<p class="flex items-center gap-1.5 text-xs text-muted-foreground">
<%= render Ui::IconComponent.new(:clock, size: :sm, class: "text-subtle") %>
07:00–08:00
</p>
<%# Botón solo de ícono: el nombre va en el botón. %>
<button type="button" aria-label="Cerrar aviso"><%= render Ui::IconComponent.new(:x, size: :sm) %></button>
# En un método call (Ruby):
def close_button
tag.button(Ui::IconComponent.svg(:x, size: :sm), type: "button", aria: { label: "Cerrar aviso" })
end
Los componentes que muestran un ícono lo piden por nombre, del mismo set:
| Componente | Parámetro | Ejemplo |
|---|---|---|
Ui::AlertComponent | icon: (o false para quitarlo) | icon: :calendar |
Ui::DropdownMenuComponent | icon: de cada opción | m.with_item_link("Editar", href: …, icon: :pencil) |
Ui::RadioCardsComponent | icon: de cada opción | { value: "card", title: "Tarjeta", icon: :credit_card } |
Ui::StatCardComponent | icon: | icon: :users |
Los nombres viejos de esas APIs siguen funcionando como alias:
:trash → :trash_2, :card → :credit_card, :check_in → :user_check, :dashboard → :layout_dashboard, :logout → :log_out, :error → :circle_alert, :success → :circle_check, :warning → :triangle_alert.
Lo que no es un ícono
Algunos gráficos se escriben aparte a propósito: se animan trazo a trazo, cambian de forma con un
resorte o son datos. No van al set ni a Ui::IconComponent. Otros ni siquiera son SVG, son
CSS: la media luna del catálogo y del estado vacío, el medidor de cupo, las barras de las gráficas,
los spinners y el asa de la hoja. Esta lista sale de buscar
<svg en los componentes y vistas del catálogo: si aparece uno sin explicación, casi seguro es un ícono pegado a mano.
| Archivo | Gráficos | Por qué no es un ícono |
|---|---|---|
action_button_component.html.erb |
2 | El arco de carga gira y la palomita de éxito se dibuja. |
input_component.html.erb |
2 | El ojo de la contraseña parpadea (el controlador redibuja el párpado) y la palomita de cada regla de fuerza se dibuja. |
stat_card_component.html.erb |
1 | La línea de tendencia (sparkline) es un dato, no un ícono. |
textarea_component.html.erb |
1 | El anillo del contador se llena conforme escribes. |
checkbox_component.rb |
1 | La palomita se dibuja trazo a trazo al marcar (pathLength). |
Agregar uno
- Búscalo en lucide.dev con el nombre en inglés. El set está tomado de
lucide-static1.48.0. - Copia solo los hijos del
<svg>(path,circle,rect…), sinstroke-widthni clases. - Ponlo en su grupo de
app/components/ui/icons.rbcon su nombre de Lucide y palabras clave en español: lo que alguien escribiría para buscarlo. - Corre
bin/rails test test/components/ui/icon_component_test.rb: revisa que el nombre sea válido y que todo el set se pinte. - Si Lucide no lo tiene, dibújalo en la misma rejilla de 24, con trazo de 2, sin rellenos y con puntas redondas, y dilo en un comentario.
Sin marcas. Los logos (WhatsApp, Instagram, Visa) tienen reglas y colores propios y no se dibujan con el trazo del set; si hacen falta, van como la imagen oficial de la marca.