Ajustes
Ajustes para un producto de IA, con un diseño como el de Cursor y Claude. Una barra lateral rellena con búsqueda, grupos y enlaces externos, tarjetas de filas con selectores discretos y opciones anidadas, una isla de guardado oscura que sube solo cuando algo cambió, ⌘S para guardar, errores de campo desde tus comprobaciones o tu servidor y estados de carga con la forma del contenido.
Cursor, Claude y Codex han acabado en la misma página de ajustes: una barra lateral rellena con búsqueda y un puñado de secciones agrupadas, y a la derecha, tarjetas de filas con una etiqueta y una descripción a la izquierda y un control discreto a la derecha. Settings es esa página. Contiene tus secciones y se ocupa de lo que toda página de ajustes hace mal: perder ediciones, guardar dos veces y el salto de una barra lateral en escritorio a una lista en un móvil.
Las filas aceptan cualquier control. SettingsSelect es el selector de valor compacto que usan esas apps, un pequeño botón con contorno que abre un menú de opciones, y SettingsNumber es un stepper que puedes mantener pulsado para repetir. Ambos se nombran con su fila, para que los lectores de pantalla oigan «Chat font, Serif». SettingsLink es una fila que abre otra cosa, con un chevron o una flecha para enlaces que salen de la app. SettingsNested despliega opciones dependientes bajo un switch, como el acceso a la red bajo Run code. La búsqueda filtra la barra lateral por etiqueta, descripción y palabras clave, y Enter abre la primera coincidencia. SettingsChoice convierte una elección en tarjetas con imagen, para que la gente elija un tema o una densidad por su aspecto.
Nada se guarda hasta que lo dices. En cuanto un valor difiere de lo guardado, una isla oscura sube desde abajo con Discard y Save, y la entrada de la sección en la barra lateral recibe un punto. Cámbialo de nuevo y la barra desaparece. Si intentas abrir otra sección, volver atrás en un móvil o cerrar la pestaña, el cambio se bloquea: la barra se sacude e indica que primero guardes o descartes, y el navegador pregunta antes de cerrar la pestaña. ⌘S o Ctrl+S guarda desde cualquier parte.
Guardar muestra su progreso en el botón, luego la isla se reduce a una marca Saved y se desliza fuera. Si tus comprobaciones fallan, los campos muestran sus errores, el foco pasa al primero y la barra indica cuántos hay que corregir. Si el servidor dice que no, devuelve errores para los campos o lanza un error, y el borrador queda exactamente como se escribió. Sigue escribiendo mientras guarda y la barra permanece para las ediciones más recientes.
En un móvil la barra lateral se convierte en una lista agrupada con descripciones y chevrons. Al tocar una sección, esta se desliza sobre la lista con un botón de volver, y el foco pasa a su encabezado. Mientras cargan los datos de una sección, muestra un skeleton con forma de filas de switches, o el tuyo mediante la prop skeleton, y un error con Try again si la carga falla.
Añade el registro Pro a components.json
components.json Añade tu token
Crea un token en tu página de cuenta y colócalo en
.env.localcomoHEXTAUI_PRO_TOKEN.Añade el bloque
pnpm dlx shadcn@latest add @hextaui-pro/settings
Conecta una sección a tu API
useSettingsForm mantiene un borrador de los valores que pasas. Devuelve errores de campo desde onSave para mostrarlos bajo el campo, o lanza un error para mostrar el mensaje en la barra de guardado. En ambos casos el borrador se conserva.
Una ruta por sección
Controla la sección activa con value y onValueChange para dar a cada sección su propia URL. El shell sigue bloqueando el cambio mientras algo está sin guardar, así que onValueChange solo se dispara cuando es seguro salir.
Carga y errores
Pasa status mientras cargan los datos de una sección. El skeleton espera 150ms para que las cargas rápidas nunca parpadeen, y un estado de error ofrece Try again mediante onRetry.
Anatomía
Las partes que compones, de fuera hacia dentro.
| Parte | Descripción |
|---|---|
SettingsShell | La página: la navegación de secciones, la columna de contenido, la barra de guardado y la protección contra salir con cambios sin guardar. |
SettingsSection | Una sección. Se renderiza solo mientras está abierta, con su encabezado, acciones opcionales y estados de carga o error. |
SettingsGroup | Una tarjeta con título que contiene filas, con un pie opcional para una nota sobre el grupo. |
SettingsRow | Una etiqueta, descripción y control, conectados entre sí para los lectores de pantalla, con el error del campo debajo. |
SettingsSelect | Un selector discreto para un valor de una lista corta. |
SettingsLink | Una fila que abre una página, un diálogo o un enlace externo. |
SettingsNested | Opciones dependientes que se despliegan mientras un switch padre está activado. |
SettingsNumber | Un stepper numérico con − y + que se repiten al mantener pulsado, construido sobre el Number Field de Base UI. |
SettingsChoice | Tarjetas con imagen para elegir una opción, como un tema o una densidad, con semántica de radio. |
SettingsSkeleton | El marcador de carga, configurable por filas por grupo y forma del control. |
useSettingsForm | El borrador de una sección. Registra lo que cambió, valida, guarda y conecta la sección con la barra de guardado. |
useSettingsNavigate | Abre una sección desde dentro del contenido, protegida como la barra lateral. |
SettingsShell
También acepta todas las props de div.
| Prop | Tipo | Predeterminado |
|---|---|---|
sections{ id, label, description?, icon?, group?, keywords?, href? }. Los elementos consecutivos con el mismo group comparten un encabezado. keywords ayuda a la búsqueda a encontrar una sección, y href convierte el elemento en un enlace externo. | SettingsSectionItem[] | – |
valueLa sección abierta, cuando la controlas. | string | – |
defaultValueLa sección abierta al principio. | string | first section |
onValueChangeSe llama cuando alguien abre otra sección. Nunca se llama mientras algo está sin guardar o guardándose. | (value: string) => void | – |
titleEl encabezado de la página sobre la navegación, y la etiqueta del botón de volver en móviles. | ReactNode | "Settings" |
descriptionUna línea bajo el título. | ReactNode | – |
navHeaderContenido en la parte superior de la barra lateral, como un enlace Back a la app. | ReactNode | – |
searchableAñade un campo de búsqueda sobre las secciones. | boolean | false |
navFooterContenido fijado al fondo de la barra lateral, como el usuario con sesión iniciada. | ReactNode | – |
groupLabelsMuestra el nombre de cada grupo sobre él. Desactívalo para separar los grupos solo con espacio; los nombres siguen etiquetando los grupos para los lectores de pantalla. | boolean | true |
SettingsSection
También acepta todas las props de section.
| Prop | Tipo | Predeterminado |
|---|---|---|
idCoincide con un id de sections. | string | – |
titleEl encabezado. | ReactNode | the section's label |
descriptionLa línea bajo el encabezado. | ReactNode | the section's description |
actionsBotones junto al encabezado. | ReactNode | – |
statusMuestra un skeleton o un error en lugar de los children. | "ready" | "loading" | "error" | "ready" |
skeletonQué mostrar mientras status es loading. | ReactNode | <SettingsSkeleton /> |
errorEl mensaje del estado de error. | ReactNode | – |
onRetryAñade Try again al estado de error. | () => void | – |
| Prop | Tipo | Predeterminado |
|---|---|---|
titleEncabezado sobre la tarjeta. | ReactNode | – |
descriptionUna línea atenuada bajo el encabezado, para indicar de qué trata el grupo. | ReactNode | – |
footerUna franja atenuada al pie de la tarjeta, para notas como qué afecta un cambio. | ReactNode | – |
| Prop | Tipo | Predeterminado |
|---|---|---|
labelEtiqueta el control dentro de la fila. | ReactNode | – |
descriptionTexto de ayuda, leído junto con el control. | ReactNode | – |
errorMarca el control como inválido y muestra el mensaje bajo la fila. | string | – |
layoutauto coloca el control junto a la etiqueta cuando la tarjeta es ancha y debajo cuando es estrecha. inline lo mantiene junto a la etiqueta, para switches. stacked siempre lo coloca debajo, para áreas de texto. | "auto" | "inline" | "stacked" | "auto" |
disabledDeshabilita el campo de la fila. | boolean | false |
SettingsSelect
También acepta todas las props de Button.
| Prop | Tipo | Predeterminado |
|---|---|---|
valueEl valor elegido. | string | – |
onValueChangeSe llama con el nuevo valor. | (value: string) => void | – |
optionsLas opciones, en orden. | { value, label }[] | – |
SettingsChoice
Un radio group, así que las teclas de flecha se mueven entre tarjetas. También acepta todas las props de RadioGroup de Base UI.
| Prop | Tipo | Predeterminado |
|---|---|---|
valueLa opción elegida. | string | – |
onValueChangeSe llama con la nueva opción. | (value: string) => void | – |
optionsLa imagen de cada tarjeta y el nombre debajo. | { value, label, preview }[] | – |
columnsTarjetas por fila. 4 pasa a 2 cuando la fila es estrecha. | 2 | 3 | 4 | 3 |
ratioVistas previas 16:10, o 2:1 para las más cortas. | "card" | "wide" | "card" |
SettingsNumber
También acepta todas las props de NumberField.Root de Base UI, como format y smallStep.
| Prop | Tipo | Predeterminado |
|---|---|---|
valueEl número actual. | number | null | – |
onValueChangeSe llama cuando cambia el número. | (value: number | null) => void | – |
minValor mínimo. El botón − se deshabilita ahí. | number | – |
maxValor máximo. El botón + se deshabilita ahí. | number | – |
stepCuánto cambia con cada pulsación o tecla de flecha. | number | 1 |
SettingsLink
También acepta todas las props de anchor. Renderiza un botón cuando no hay href.
| Prop | Tipo | Predeterminado |
|---|---|---|
labelEl título de la fila. | ReactNode | – |
descriptionUna línea bajo el título. | ReactNode | – |
externalAbre href en una pestaña nueva y muestra una flecha en lugar de un chevron. | boolean | false |
| Prop | Tipo | Predeterminado |
|---|---|---|
openMuestra las opciones. Normalmente el valor del switch padre. | boolean | – |
| Prop | Tipo | Predeterminado |
|---|---|---|
groupsCuántas filas tiene cada grupo de marcadores. | number[] | [3, 2] |
controlLa forma a la derecha de cada fila. | "switch" | "select" | "input" | "switch" |
useSettingsForm
Devuelve { values, setValue, errors, dirty, status, save, discard }.
| Prop | Tipo | Predeterminado |
|---|---|---|
valuesLo guardado ahora. Cuando cambia y no hay ediciones, el borrador lo sigue. | Values | – |
onSaveGuarda el borrador. Devuelve { field: message } para mostrar errores de campo, o lanza un error para mostrar el mensaje en la barra de guardado. | (values) => void | errors | Promise<void | errors> | – |
validateSe ejecuta antes de onSave. Cualquier error detiene el guardado y enfoca el primer campo inválido. | (values) => errors | undefined | – |
useSettingsNavigate
Devuelve una función que abre una sección desde cualquier parte dentro del shell, como el botón Open de un banner. Respeta los cambios sin guardar igual que la barra lateral.
| Prop | Tipo | Predeterminado |
|---|---|---|
navigateAbre la sección, o sacude la barra de guardado si algo está sin guardar. | (id: string) => void | – |
| Key | Acción |
|---|---|
| Tab | Recorre la navegación, luego la sección y luego la barra de guardado cuando está abierta. |
| Enter | Abre la sección enfocada. |
| ↑↓ | En un stepper, cambia el número en un paso. Shift avanza de diez en diez. |
| Enter | En el campo de búsqueda, abre la primera sección coincidente. Escape borra la búsqueda. |
| ⌘S | Guarda mientras haya cambios sin guardar. Ctrl+S en Windows y Linux. |
- La navegación es un landmark, y la sección abierta se marca como la página actual.
- Cada sección es una región con el nombre de su encabezado. En móviles, el foco pasa al encabezado cuando se abre una sección y vuelve a su fila al regresar.
- Las filas usan Field, así que las etiquetas, descripciones y errores están asociados al control.
- La navegación bloqueada se anuncia de forma polite, y un guardado fallido se anuncia como una alerta.
- La barra de guardado y cualquier panel oculto son inert, así que quedan fuera del orden de tabulación y ocultos para los lectores de pantalla.
- Con movimiento reducido, los paneles se desvanecen en lugar de deslizarse y la sacudida de la barra de guardado se convierte en un anillo.
Construido con
Los componentes gratuitos de HextaUI con los que está hecho Settings. Cada uno se instala por separado.
Código
6 archivos, añadidos a components/blocks/settings.