Checkbox en HTML: value, label, estilos y accesibilidad de verdad

Casillas y botones de radio parecen triviales y casi todo el mundo los hace mal. Qué llega de verdad al servidor, por qué el label no es decorativo y cómo estilarlos en 2026 sin romper el teclado.

Una lista de casillas dibujada en papel, con una marcada en rojo
Tabla de contenidos

Escribes un formulario de alta. Metes una casilla de "Acepto la política de privacidad", un par de checkboxes de preferencias y un grupo de radios para el plan. Funciona: se ven, se marcan, el formulario envía. Y aun así, casi seguro, hay tres cosas mal. El área que se puede pulsar es un cuadrado de trece píxeles. Cuando el usuario deja una casilla sin marcar, al backend no le llega false: no le llega nada. Y el día que quieras estilarlas, el primer tutorial que encuentres te va a decir que ocultes el input con display: none, que es justo lo que rompe el teclado.

Un checkbox en HTML es de los controles más simples que existen y de los que más gente implementa a medias. No porque sea difícil, sino porque "se ve bien" y ahí se para la revisión. Lo que sigue es lo que pasa por debajo: qué se envía de verdad, cómo se lee en el servidor, por qué el label no es decorativo y cómo se estilan sin cargarte la accesibilidad. Lo que la documentación menciona de pasada y casi ningún tutorial en español junta en un sitio.

Checkbox y radio: la diferencia conceptual que decide cuál usar

Antes de la sintaxis, la decisión de diseño. Un checkbox representa una opción independiente y binaria: está marcada o no, y su estado no depende de las demás. Tres checkboxes son tres preguntas de sí/no sin relación entre ellas. "¿Quieres newsletter?", "¿Aceptas los términos?", "¿Guardar la sesión?". Cada una vive su vida.

Un radio representa una elección dentro de un conjunto mutuamente excluyente. No tiene sentido un radio solo: siempre es "una de estas N". Plan mensual o anual. Envío estándar o urgente. Marcar una opción del grupo deselecciona automáticamente la que estuviera marcada, sin que escribas una línea de JavaScript. Ese comportamiento es gratis y viene del navegador.

La regla práctica: si el usuario puede querer cero, una o varias respuestas, son checkboxes. Si tiene que elegir exactamente una, son radios. Y si son radios pero "ninguna" es una respuesta válida, o añades una opción explícita "Ninguna", o replanteas si no era un <select>. Un grupo de radios sin opción neutra obliga a elegir en cuanto el usuario toca uno: no hay forma nativa de volver a "nada seleccionado" salvo recargar.

El atributo name: por qué el radio lo comparte y el checkbox a veces lleva name="x[]"

El name es lo que conecta el control con el mundo. Sin name, el input no se envía: es como si no existiera para el formulario. Es el error silencioso más común, porque el control se ve y se marca igual, pero el dato nunca llega.

En un grupo de radios, el name es lo que define el grupo. Radios con el mismo name son mutuamente excluyentes; radios con name distinto son grupos distintos. Lo dice MDN sin rodeos: "Un grupo de radios se define dando a cada botón del grupo el mismo name". Si tres radios comparten name="plan", el navegador sabe que son la misma pregunta y solo deja marcar uno.

<input type="radio" name="plan" value="mensual" id="plan-mensual">
<label for="plan-mensual">Mensual</label>

<input type="radio" name="plan" value="anual" id="plan-anual">
<label for="plan-anual">Anual</label>

Con checkboxes es distinto. Cada checkbox es independiente, así que normalmente cada uno lleva su propio name. Pero cuando quieres recoger varias marcas de la misma lista, "elige tus intereses", compartes name entre ellas. Aquí aparece el detalle que casi nadie explica: la sintaxis name="intereses[]" con corchetes.

<input type="checkbox" name="intereses[]" value="backend" id="i-backend">
<label for="i-backend">Backend</label>

<input type="checkbox" name="intereses[]" value="frontend" id="i-frontend">
<label for="i-frontend">Frontend</label>

<input type="checkbox" name="intereses[]" value="devops" id="i-devops">
<label for="i-devops">DevOps</label>

Los corchetes no son HTML: HTML se limita a mandar el nombre tal cual. Son una convención de PHP (y de frameworks que la imitan) para que el backend agrupe los valores repetidos en un array en lugar de que el último pise al anterior. Lo desgloso en el servidor, más abajo.

value: sin él llega "on" (y por qué eso no le sirve a nadie)

El value es lo que se envía cuando la casilla está marcada. Aquí está la trampa que MDN documenta y que mucha gente ignora: "Si se omite el atributo value, el valor por defecto del checkbox es on". Es decir, este control:

<input type="checkbox" name="newsletter">

envía newsletter=on cuando está marcado. on no es un dato: no distingue una casilla de otra, no se guarda en base de datos con sentido, no te dice qué representaba. Para un checkbox único de "sí/no" a veces basta con detectar su presencia (está o no está), pero en cuanto tienes varias casillas compartiendo name, sin value explícito todas mandan on y no puedes saber cuál era cuál.

Regla: pon siempre value. En radios es obligatorio de facto por lo mismo, sin value, todo el grupo enviaría on y perderías la información de qué se eligió. El value es el identificador estable del dato; el estado marcado solo decide si ese dato viaja o no.

checked, y el estado indeterminado que solo existe por JavaScript

checked es un atributo booleano que marca la casilla de salida, antes de que el usuario toque nada:

<input type="checkbox" name="recordar" value="1" checked>

Basta su presencia. checked="false" no desmarca nada: en HTML los atributos booleanos se activan por estar presentes, no por su valor. Si el atributo está, la casilla arranca marcada; si no está, arranca vacía. En radios, checked marca la opción por defecto del grupo.

Y luego está el tercer estado, el que casi nadie usa bien: el indeterminado. Un checkbox no tiene dos estados visuales, tiene tres: marcado, sin marcar e indeterminado (ese guion o cuadrado relleno a medias). El detalle crítico, literal de MDN: "se establece mediante la propiedad indeterminate del objeto HTMLInputElement vía JavaScript (no se puede fijar con un atributo HTML)". No existe <input indeterminate>. Solo se pone por código.

document.querySelector('#seleccionar-todo').indeterminate = true;

Su caso de uso real es el checkbox maestro de "seleccionar todo": cuando unas cuantas hijas están marcadas pero no todas, el maestro no debe verse ni marcado ni vacío, sino indeterminado.

<label><input type="checkbox" id="todos"> Seleccionar todo</label>
<label><input type="checkbox" class="hijo" name="items[]" value="1"> Item 1</label>
<label><input type="checkbox" class="hijo" name="items[]" value="2"> Item 2</label>
<label><input type="checkbox" class="hijo" name="items[]" value="3"> Item 3</label>
const maestro = document.querySelector('#todos');
const hijos = [...document.querySelectorAll('.hijo')];

function sincronizar() {
  const marcados = hijos.filter(h => h.checked).length;
  maestro.checked = marcados === hijos.length;
  maestro.indeterminate = marcados > 0 && marcados < hijos.length;
}

maestro.addEventListener('change', () => {
  hijos.forEach(h => (h.checked = maestro.checked));
});
hijos.forEach(h => h.addEventListener('change', sincronizar));

Ojo a una consecuencia que sorprende: el estado indeterminado es puramente visual. Al enviar el formulario, un checkbox indeterminado se comporta según su checked real. Si indeterminate = true pero checked = false, no se envía. El indeterminado informa al usuario; no cambia lo que viaja al servidor.

El error número uno: no asociar el label (y por qué tu área clicable es un cuadrado de 13 px)

Este es el fallo que veo en más formularios de producción, y el que más molesta al usuario sin que nadie sepa por qué. Un checkbox nativo mide alrededor de trece píxeles de lado. Si el texto de al lado es solo texto suelto, un <span>, un <div>, una <label> sin asociar, entonces la única zona pulsable es ese cuadrado diminuto. En un móvil, apuntar a trece píxeles con el pulgar es un ejercicio de puntería.

Las WCAG 2.2 lo formalizan en el criterio 2.5.8, "Target Size (Minimum)", nivel AA: un objetivo táctil debe medir al menos 24 por 24 píxeles CSS. El checkbox nativo se queda a la mitad. La forma correcta y gratuita de cumplirlo no es agrandar el input con CSS raro, sino asociar el label: al hacerlo, todo el texto de la etiqueta se vuelve parte del área que activa la casilla. De trece píxeles pasas a toda la línea de texto.

Asociar el label no es cosmética de accesibilidad. Es que el usuario pueda dar en el blanco. Y de paso, un lector de pantalla anuncia el texto de la etiqueta al enfocar el control, en vez de leer "casilla, sin etiqueta".

Una persona rellena a mano un formulario sujeto a una carpeta con pinza
En papel, la casilla sin marcar se ve. En HTML no se envía, y ahí empieza el problema.

label con for o envolviendo el input: las dos formas correctas

Hay exactamente dos maneras válidas de asociar un <label> a su control, y ninguna es "poner el texto cerca".

Explícita, con for e id. El for del label apunta al id del input. Tienen que coincidir y el id debe ser único en la página.

<input type="checkbox" name="terminos" value="1" id="acepto-terminos">
<label for="acepto-terminos">Acepto los términos y la política de privacidad</label>

Implícita, envolviendo el input. El input va dentro del label. No hace falta for ni id: la asociación es por anidamiento.

<label>
  <input type="checkbox" name="terminos" value="1">
  Acepto los términos y la política de privacidad
</label>

Las dos funcionan igual de bien para el clic y para el lector de pantalla. La envolvente es más compacta y no obliga a inventar id únicos; la explícita da más libertad para maquetar, porque input y texto pueden estar en ramas distintas del DOM. Uso la envolvente por defecto y la explícita cuando la maqueta separa físicamente el control del texto. Lo que no vale nunca es un label sin for que no envuelva a nada: eso es un párrafo disfrazado.

Qué llega de verdad al servidor: la casilla NO marcada no se envía

Aquí está el malentendido que rompe backends. La intuición dice que un checkbox manda true cuando está marcado y false cuando no. Es falso. Lo dice MDN con todas las letras: "Si un checkbox está sin marcar cuando se envía su formulario, no se envían al servidor ni el nombre ni el valor. No hay ninguna forma en HTML de representar el estado de no-marcado de un checkbox".

Traducido: una casilla marcada envía name=value. Una casilla sin marcar no envía nada en absoluto. No llega newsletter=false, no llega newsletter=off, no llega newsletter=. Sencillamente esa clave no aparece en los datos del formulario. El servidor tiene que deducir "no marcado" por ausencia, no por valor.

Esto tiene consecuencias reales. Si actualizas un registro leyendo solo lo que llega, un checkbox que el usuario desmarcó no aparecerá en la petición, y si tu código hace "actualiza solo los campos presentes", ese campo se queda con el valor viejo. El usuario lo desmarcó, tú no te enteraste. Es el mismo tipo de fallo silencioso del que hablo en por qué NULL no es menor que una fecha: la ausencia de dato no es lo mismo que un dato en cero, y tratarlos igual te muerde en producción.

Con los grupos de radios pasa algo parecido: "si no hay ningún radio seleccionado al enviar el formulario, el grupo no se incluye en absoluto en los datos enviados, porque no hay valor que reportar". De nuevo, ausencia.

Leerlo en el backend: isset() en PHP y varias casillas con name="x[]"

Como el navegador comunica "no marcado" por ausencia de la clave, en PHP no compruebas el valor, compruebas la presencia. Con isset():

$newsletter = isset($_POST['newsletter']);   // true si marcado, false si no

Si el campo lleva value="1", $_POST['newsletter'] valdrá "1" cuando esté marcado, y la clave no existirá cuando no lo esté. isset() te devuelve directamente el booleano que querías. Nunca hagas if ($_POST['newsletter'] == false): si no está marcado, esa clave no existe y disparas un warning de índice indefinido.

Para varias casillas con name="intereses[]", PHP construye el array por ti gracias a los corchetes:

$intereses = $_POST['intereses'] ?? [];   // ['backend', 'devops'] si marcó esas dos

foreach ($intereses as $interes) {
    // guardar cada uno
}

El ?? [] es imprescindible: si el usuario no marca ninguna casilla del grupo, la clave intereses no llega y sin el operador de coalescencia tendrías un array indefinido. Lo que MDN describe a nivel de HTTP, "llegan como pares repetidos, intereses=backend&intereses=devops, y hay que capturarlos todos, no solo el último", PHP lo resuelve precisamente con esa sintaxis de corchetes. En otros lenguajes sin esa convención (Node con URLSearchParams, por ejemplo) tú mismo recoges todos los valores repetidos con getAll('intereses'), porque get() te daría solo el primero.

Un checkbox único que quieras guardar como booleano en base de datos se normaliza así:

$acepta = isset($_POST['terminos']) ? 1 : 0;

Nunca confíes en que llegue el 0. El 0 lo pones tú cuando la clave falta.

required en un grupo de radios: obligar a elegir una

En un checkbox suelto, required hace lo esperable: si no está marcado, el formulario no se envía. MDN lo ata a la validación: "Si el checkbox tiene el atributo required pero no está marcado, ValidityState.valueMissing será true". Útil para el clásico "Debes aceptar los términos".

<label>
  <input type="checkbox" name="terminos" value="1" required>
  Acepto los términos
</label>

En un grupo de radios hay un detalle que casi ningún tutorial menciona y que es puro regalo: basta poner required en uno de los radios para obligar a marcar todo el grupo. Literal de MDN: "Si cualquier radio de un grupo con el mismo nombre tiene el atributo required, hay que marcar un radio de ese grupo, aunque no tiene por qué ser el que lleva el atributo".

<fieldset>
  <legend>Plan de suscripción</legend>

  <label><input type="radio" name="plan" value="mensual" required> Mensual</label>
  <label><input type="radio" name="plan" value="anual"> Anual</label>
</fieldset>

Con ese único required, el navegador no deja enviar hasta que se elija mensual o anual. No necesitas repetirlo en todos ni ponerlo en el que esté marcado por defecto. Un atributo, un grupo entero validado, cero JavaScript.

Estilarlos en 2026: accent-color para lo sencillo

Durante años, personalizar un checkbox obligaba a ocultarlo y dibujar uno falso. Hoy, si lo único que quieres es cambiar el color del control nativo para que pegue con tu marca, hay una línea:

input[type="checkbox"],
input[type="radio"] {
  accent-color: #2563eb;
}

accent-color tiñe el relleno del control nativo respetando su comportamiento, su foco y su accesibilidad. Según MDN se aplica a checkbox, radio, range y progress, y su valor por defecto es auto (el color del sistema). La contrapartida honesta: MDN la marca como de disponibilidad limitada, "no es Baseline porque no funciona en algunos de los navegadores más usados". En la práctica está en Chrome desde la versión 93, Firefox desde la 92 y Safari desde la 15.4, así que en navegadores actuales la tienes; en versiones viejas simplemente se cae al color por defecto, que es una degradación perfectamente aceptable. Si solo quieres color, accent-color es la respuesta correcta en 2026 y no deberías montar nada más complejo. Estilar de más es una forma de deuda técnica.

Personalización total con appearance:none, :checked y ::before

Cuando el diseño exige un control que no se parece al nativo, un cuadrado redondeado con un check dibujado, tamaños concretos, animación, entonces sí toca reconstruirlo. La forma moderna no oculta el input: lo desnuda con appearance: none y lo vuelve a pintar. Sigue siendo un checkbox real, enfocable y navegable por teclado; solo cambia su piel.

input[type="checkbox"].custom {
  appearance: none;
  -webkit-appearance: none;
  width: 1.4rem;
  height: 1.4rem;
  border: 2px solid #94a3b8;
  border-radius: 0.35rem;
  display: inline-grid;
  place-content: center;
  cursor: pointer;
}

input[type="checkbox"].custom::before {
  content: "";
  width: 0.75rem;
  height: 0.75rem;
  transform: scale(0);
  transition: transform 0.1s ease-in-out;
  box-shadow: inset 1rem 1rem #2563eb;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}

input[type="checkbox"].custom:checked::before {
  transform: scale(1);
}

input[type="checkbox"].custom:checked {
  border-color: #2563eb;
}

La clave es que el ::before con el clip-path en forma de tick vive dentro del propio input, escalado a cero cuando no está marcado y a uno cuando sí, gobernado por el pseudoselector :checked. No hay input oculto, no hay label haciendo de control falso. El elemento sigue siendo el de verdad, con lo cual conserva foco, teclado y semántica.

Por qué NUNCA se oculta el input con display:none (y la técnica correcta)

Este es el consejo que más tutoriales dan mal, incluidos vídeos con muchas visitas. El patrón "checkbox personalizado" clásico dice: input { display: none; } y dibuja un control falso en el label. No lo hagas.

display: none y visibility: hidden sacan el elemento del árbol de accesibilidad y del orden de tabulación. Un input oculto así deja de ser enfocable: el usuario que navega con Tab no llega nunca a él, el lector de pantalla no lo anuncia, y la tecla Espacio no lo marca porque no hay nada que enfocar. Has convertido un control accesible en una imagen bonita que solo funciona con ratón. Para quien usa teclado o lector de pantalla, tu formulario está roto.

Si por lo que sea necesitas apoyarte en un control dibujado aparte (porque appearance: none no te da el resultado), hay dos técnicas que mantienen el input enfocable:

La primera, superponer el input real, transparente, encima del control visual, para que reciba el foco y los clics aunque no se vea:

.control-visual { position: relative; }
.control-visual input {
  position: absolute;
  inset: 0;
  opacity: 0;      /* invisible pero presente y enfocable */
  cursor: pointer;
}

La segunda, la clase sr-only (solo lectores de pantalla), que esconde visualmente el input pero lo mantiene en el árbol de accesibilidad y en el tab order:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

La diferencia con display: none es total: opacity: 0 y el recorte de sr-only no retiran el elemento del foco ni del teclado; display: none sí. Si vas a ocultar un input, esta es la única forma que no rompe la navegación. Dicho esto, en 2026 la ruta con appearance: none sobre el propio input te evita todo este baile en la mayoría de casos: solo bajo a esta técnica cuando el diseño lo impone.

Accesibilidad: fieldset y legend, :focus-visible y qué anuncia NVDA

Un grupo de radios necesita un contexto que diga "estas opciones van juntas y esto es lo que preguntan". Ese contexto es <fieldset> con <legend>. MDN los usa en todos sus ejemplos de radio por una razón: sin ellos, un lector de pantalla anuncia "Mensual, botón de radio" sin decir de qué grupo es. Con ellos, anuncia la leyenda como contexto: "Plan de suscripción, Mensual, botón de radio, 1 de 2".

<fieldset>
  <legend>¿Cómo prefieres el envío?</legend>

  <label><input type="radio" name="envio" value="estandar" checked> Estándar (3-5 días)</label>
  <label><input type="radio" name="envio" value="urgente"> Urgente (24 h)</label>
</fieldset>

El foco visible es la otra mitad. Nunca hagas outline: none sin sustituto: dejas al usuario de teclado sin saber dónde está. Lo correcto en 2026 es :focus-visible, que muestra el anillo cuando se navega con teclado y no lo muestra en un clic de ratón, que era la molestia que llevó a mucha gente a quitarlo del todo.

input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

Sobre qué se oye: un lector como NVDA anuncia el rol ("casilla", "botón de radio"), el texto del label asociado y el estado ("marcado" / "no marcado"). Si el label no está asociado, se pierde el texto y el usuario oye una casilla anónima. Si no hay fieldset/legend, se pierde el grupo. Toda la accesibilidad de estos controles se sostiene sobre las mismas tres piezas: label asociado, agrupación semántica y foco visible. Ninguna cuesta esfuerzo; todas se olvidan.

Validación nativa sin JavaScript

El navegador valida checkboxes y radios sin una línea de código, y conviene aprovecharlo antes de escribir validación propia. required en un checkbox bloquea el envío si no está marcado; required en un radio del grupo obliga a elegir uno. El navegador muestra su propio mensaje y enfoca el control fallido.

Si quieres reaccionar a la validación sin sustituirla, la API de restricciones te da el estado:

const form = document.querySelector('#alta');
const terminos = form.elements.terminos;

// Un mensaje personalizado deja el campo inválido HASTA que lo limpias a mano.
// Si no lo borras, el navegador deja de disparar `submit` incluso con la casilla
// marcada, y el formulario no se envía nunca más. Por eso se limpia primero.
terminos.addEventListener('change', () => terminos.setCustomValidity(''));

form.addEventListener('submit', (e) => {
  terminos.setCustomValidity('');

  if (!terminos.checked) {
    e.preventDefault();
    terminos.setCustomValidity('Necesito que aceptes los términos para continuar.');
    terminos.reportValidity();
  }
});

Ese setCustomValidity('') de la primera línea del manejador no es paranoia. Es la diferencia entre un formulario que funciona y uno que, después del primer intento fallido, no se envía jamás. Es un fallo que no se ve en la demo y que aparece en cuanto un usuario real se equivoca una vez.

Aun así, la regla de oro no cambia: la validación de cliente es experiencia de usuario, no seguridad. Cualquiera puede saltarse el HTML y mandar la petición a mano. El required mejora el formulario para quien lo rellena de buena fe, pero la comprobación que de verdad protege tus datos, que los términos se aceptaron, que el plan existe, que el interés está en la lista permitida, va siempre en el servidor, con isset() y validando que el value recibido es uno de los que tú ofreciste. Un checkbox marcado en el navegador es una intención; solo lo que confirmas en el backend es un hecho.

Preguntas frecuentes

¿Cómo se pone un checkbox en HTML?

Con un elemento input de tipo checkbox y, muy importante, un atributo name y un value: . El name es lo que hace que se envíe (sin name no viaja nada) y el value es lo que llega al servidor cuando está marcado. Añade siempre un label asociado para que se pueda pulsar el texto, no solo el cuadrito: .

¿Qué se envía al servidor cuando una casilla no está marcada?

Nada. Según MDN, si un checkbox está sin marcar al enviar el formulario no se envían ni el nombre ni el valor: no existe forma en HTML de representar el estado no marcado. No llega false ni off ni una clave vacía; simplemente esa clave no aparece en la petición. El backend deduce el no marcado por ausencia de la clave, no por su valor. Por eso en PHP se comprueba con isset() y el 0 lo pones tú cuando la clave falta.

¿Cómo se lee un checkbox en PHP?

Con isset(), porque lo que llega es presencia o ausencia, no un booleano. Un checkbox marcado con value="1" da $_POST['campo'] === '1'; sin marcar, la clave no existe. Se normaliza así: $acepta = isset($_POST['terminos']) ? 1 : 0;. Nunca hagas comparaciones tipo $_POST['terminos'] == false, porque si no está marcado la clave no existe y disparas un warning de índice indefinido.

¿Cómo recojo varias casillas marcadas a la vez?

Dales a todas el mismo name con corchetes: name="intereses[]". Los corchetes son una convención de PHP para que el backend agrupe los valores repetidos en un array en vez de que el último pise al anterior. Se lee con $intereses = $_POST['intereses'] ?? [];. El ?? [] es imprescindible: si el usuario no marca ninguna, la clave no llega. A nivel HTTP los valores viajan como pares repetidos (intereses=backend&intereses=devops); en Node con URLSearchParams se recogen todos con getAll(), no con get().

¿Cómo hago obligatorio un grupo de botones de radio?

Basta poner required en uno solo de los radios del grupo. Según MDN, si cualquier radio de un grupo con el mismo name lleva required, hay que marcar uno del grupo, y no tiene por qué ser el que lleva el atributo. No necesitas repetirlo en todos ni ponerlo en el marcado por defecto: con un único required, el navegador no deja enviar el formulario hasta que se elija una opción, sin una línea de JavaScript.

¿Cómo personalizo el color de un checkbox con CSS sin romper la accesibilidad?

Si solo quieres cambiar el color, usa accent-color: #2563eb; sobre el input. Tiñe el control nativo respetando foco, teclado y accesibilidad, y funciona en Chrome 93+, Firefox 92+ y Safari 15.4+. Si necesitas rediseñar la forma, usa appearance: none sobre el propio input y píntalo con ::before y :checked, sin ocultarlo. Lo que nunca debes hacer es display: none para esconder el input: lo saca del orden de tabulación y del árbol de accesibilidad, con lo que pierdes el foco y la navegación por teclado. Si tienes que ocultarlo, usa opacity: 0 o una clase sr-only, que lo mantienen enfocable.

¿Te ha servido?

Si tienes un problema parecido y quieres una opinión técnica, cuéntamelo. Te respondo en menos de 24 h laborables.

WhatsApp