Cómo validar un formulario con JavaScript

Validación nativa de HTML5, la Constraint Validation API, mensajes de error accesibles y un ejemplo completo con demo funcional para copiar.

Validar un formulario con JavaScript significa comprobar, antes de enviar los datos, que cada campo cumple sus reglas: que no esté vacío, que el email tenga formato correcto, que la contraseña sea segura. Lo primero es apoyarte en la validación nativa de HTML5 (required, type, pattern) y luego usar la Constraint Validation API de JavaScript para personalizar los mensajes y controlar cuándo se muestran. En esta guía vas a montar las dos capas juntas, con ejemplos reales y una demo que funciona de verdad.

Si ya tienes un formulario montado y solo estabas mostrando un alert() genérico cuando algo fallaba, esto te va a cambiar la forma de trabajar. Aquí es donde la mayoría se atasca: usan solo required y luego se preguntan por qué el mensaje de error se ve distinto en Chrome que en Firefox, o por qué no pueden traducirlo al español.

Validación nativa de HTML5 primero

Antes de escribir una línea de JavaScript, el navegador ya sabe validar bastante por sí solo. Estos atributos existen desde hace más de una década y funcionan en cualquier navegador moderno:

HTML 📋
<input type="email" required>
<input type="text" minlength="3" maxlength="50" required>
<input type="number" min="18" max="99">
<input type="tel" pattern="[0-9]{7,15}">
<input type="text" pattern="[A-Za-z0-9]+" title="Solo letras y números">

required obliga a rellenar el campo. type="email" exige el formato básico de un correo (algo@algo.algo). minlength/maxlength limitan caracteres en campos de texto. min/max hacen lo mismo con números. pattern acepta una expresión regular para lo que no cubren los tipos estándar.

Si envías el formulario tal cual, sin ningún JavaScript, el navegador ya bloquea el envío y muestra un globo con el error. Pruébalo: crea un HTML con un <input required> dentro de un <form> y dale a enviar vacío. Va a funcionar solo.

El problema es que ese mensaje nativo no lo puedes traducir del todo, no puedes controlar su estilo, y en formularios largos aparece uno detrás de otro en vez de mostrar todos los errores a la vez. Para eso necesitas la Constraint Validation API. Si quieres ver primero todo lo que da de sí HTML5 por su cuenta, con tipos de input y ARIA incluidos, tenemos un tutorial dedicado a la validación nativa HTML5 y accesibilidad de formularios.

La Constraint Validation API: la pieza que casi nadie usa bien

Todo elemento de formulario tiene un objeto validity con el resultado de comprobar sus reglas HTML5. Y tiene tres métodos que vas a usar constantemente:

JavaScript 📋
const campoEmail = document.getElementById('email');

campoEmail.addEventListener('input', function() {
    if (campoEmail.validity.valueMissing) {
        campoEmail.setCustomValidity('Este campo es obligatorio');
    } else if (campoEmail.validity.typeMismatch) {
        campoEmail.setCustomValidity('Escribe un email con formato correcto');
    } else {
        campoEmail.setCustomValidity(''); // limpia el error si ya es válido
    }
});

El objeto validity trae propiedades booleanas útiles: valueMissing (campo vacío pero obligatorio), typeMismatch (no cumple el type, por ejemplo un email sin arroba), tooShort/tooLong (por minlength/maxlength), rangeUnderflow/rangeOverflow (por min/max), y patternMismatch (no cumple el pattern). Con esto puedes construir un mensaje distinto para cada motivo de error, en vez de un genérico "campo inválido" que no le dice nada al usuario.

💡 Consejo:

La API de validación funciona en el 97% de navegadores en uso según Can I Use. No hace falta ninguna librería ni polyfill para usarla hoy.

Validar un formulario HTML en tiempo real sin molestar al usuario

Si validas en el evento input desde la primera letra, el usuario ve "email inválido" mientras todavía está escribiendo la primera parte de su correo. Es molesto y hace que el formulario se sienta roto. La solución que usan los formularios bien hechos es una combinación de dos eventos:

JavaScript 📋
function activarValidacionEnTiempoReal(campo, funcionValidadora) {
    let yaSeValido = false;

    campo.addEventListener('blur', function() {
        yaSeValido = true;
        funcionValidadora();
    });

    campo.addEventListener('input', function() {
        if (yaSeValido) {
            funcionValidadora();
        }
    });
}

Para el estilo visual, en 2026 ya no hace falta que tú mismo añadas y quites clases .error/.success a mano para todo: CSS tiene las pseudoclases :user-invalid y :user-valid, que aplican automáticamente después de que el usuario haya interactuado con el campo (a diferencia de :invalid, que se aplica desde el primer render, incluso con el campo vacío). Soporte amplio desde noviembre de 2023: Chrome/Edge 119+, Firefox 88+, Safari 16.5+, más del 93% de navegadores en uso según Can I Use.

CSS 📋
input:user-invalid {
    border-color: #ef4444;
    background-color: #fef2f2;
}

input:user-valid {
    border-color: #10b981;
    background-color: #f0fdf4;
}

Como es una funcionalidad relativamente reciente, no la des por garantizada en proyectos que necesiten soportar navegadores muy antiguos: en ese caso sigue añadiendo las clases manualmente con JavaScript, como vas a ver en el ejemplo completo. Las dos técnicas se pueden usar juntas sin problema.

Casos prácticos con código completo

Vamos con los campos que aparecen en el 90% de los formularios reales. Cada uno es independiente y lo puedes copiar suelto.

Validar email con JavaScript

La base es type="email" más checkValidity(). No hace falta escribir tu propia regex para esto: el navegador ya sabe reconocer el formato básico de un correo (usuario@dominio.extensión).

JavaScript 📋
function validarEmail(input, spanError) {
    if (input.validity.valueMissing) {
        spanError.textContent = 'El email es obligatorio';
    } else if (input.validity.typeMismatch) {
        spanError.textContent = 'Escribe un email válido (ejemplo: nombre@dominio.com)';
    } else {
        spanError.textContent = '';
    }
    return input.checkValidity();
}

Si en tu proyecto necesitas una comprobación extra que HTML5 no cubre (por ejemplo, bloquear direcciones de dominios desechables), añade una regex sencilla encima, no en lugar de type="email":

JavaScript 📋
const regexEmailBasica = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

Esa regex es deliberadamente simple: solo comprueba que haya texto, una arroba, más texto, un punto y más texto. No valida contra la especificación completa del formato de email (esa regex existe y tiene más de 6.000 caracteres, y ni así cubre todos los casos reales). Para uso normal es más que suficiente, y el motivo real por el que sabes si un email existe es enviándole un correo de confirmación, no con una regex más larga.

Validar contraseña segura con reglas visibles

Lo que espera el usuario hoy no es un mensaje de "contraseña inválida" después de enviar el formulario. Es ver, mientras escribe, qué reglas ya cumple y cuáles le faltan. Vamos a construir eso.

HTML 📋
<div class="form-group">
    <label for="password">Contraseña</label>
    <input type="password" id="password" minlength="8" required
           aria-describedby="password-rules">
    <button type="button" id="toggle-password" aria-pressed="false">
        Mostrar
    </button>
    <ul id="password-rules">
        <li data-rule="length">Al menos 8 caracteres</li>
        <li data-rule="upper">Una mayúscula</li>
        <li data-rule="number">Un número</li>
    </ul>
</div>
JavaScript 📋
const campoPassword = document.getElementById('password');
const reglas = document.querySelectorAll('#password-rules li');

campoPassword.addEventListener('input', function() {
    const valor = campoPassword.value;

    const cumple = {
        length: valor.length >= 8,
        upper: /[A-Z]/.test(valor),
        number: /[0-9]/.test(valor)
    };

    reglas.forEach(function(li) {
        const regla = li.dataset.rule;
        li.classList.toggle('fv-rule-ok', cumple[regla]);
    });

    const todasCumplidas = Object.values(cumple).every(Boolean);
    campoPassword.setCustomValidity(todasCumplidas ? '' : 'La contraseña no cumple los requisitos');
});

El botón de mostrar/ocultar contraseña necesita aria-pressed para que un lector de pantalla anuncie si está activado o no, además de cambiar su propio texto visible:

JavaScript 📋
const botonMostrar = document.getElementById('toggle-password');

botonMostrar.addEventListener('click', function() {
    const oculto = campoPassword.type === 'password';
    campoPassword.type = oculto ? 'text' : 'password';
    botonMostrar.textContent = oculto ? 'Ocultar' : 'Mostrar';
    botonMostrar.setAttribute('aria-pressed', String(oculto));
});

Un error que se ve mucho: bloquear el pegado en el campo de contraseña con onpaste="return false" para "forzar" a que la escriban a mano. No lo hagas. Rompe los gestores de contraseñas, que es justo la herramienta que hace que la gente use contraseñas seguras en primer lugar. Deja pegar siempre.

Confirmar contraseña

Aquí no hay ningún atributo HTML5 que compare dos campos entre sí, así que esto sí es JavaScript puro. Usa setCustomValidity() para que se integre con el resto de la validación nativa:

JavaScript 📋
const campoConfirmar = document.getElementById('confirm-password');

function validarConfirmacion() {
    if (campoConfirmar.value !== campoPassword.value) {
        campoConfirmar.setCustomValidity('Las contraseñas no coinciden');
    } else {
        campoConfirmar.setCustomValidity('');
    }
}

campoConfirmar.addEventListener('input', validarConfirmacion);
campoPassword.addEventListener('input', validarConfirmacion); // por si cambian la primera después

La segunda línea es la que se olvida siempre: si el usuario ya escribió la confirmación y luego vuelve a la contraseña original para corregirla, hay que revalidar la confirmación también, no solo cuando escribe ahí.

Validar teléfono (formato internacional)

La mayoría de la audiencia de este tipo de tutorial no está en un solo país, así que una regex pensada para un formato nacional concreto deja fuera a la mitad de la gente. Lo razonable es aceptar un prefijo internacional opcional y entre 7 y 15 dígitos, que es el rango que cubre prácticamente cualquier número de teléfono del mundo según el estándar E.164:

JavaScript 📋
function validarTelefono(input, spanError) {
    const limpio = input.value.replace(/[\s-]/g, ''); // quita espacios y guiones
    const regex  = /^\+?[0-9]{7,15}$/;

    if (limpio === '') {
        spanError.textContent = 'El teléfono es obligatorio';
        return false;
    }
    if (!regex.test(limpio)) {
        spanError.textContent = 'Introduce un teléfono válido (7 a 15 dígitos, con o sin +prefijo)';
        return false;
    }

    spanError.textContent = '';
    return true;
}

Si tu proyecto sí es para un solo país y quieres restringir el formato exacto, usa el atributo pattern directamente en el HTML en vez de tocar la función. Por ejemplo, para México con lada de 10 dígitos: pattern="[0-9]{10}". Cambias el patrón según el país, no la lógica de validación.

El replace() que limpia espacios y guiones es uno más de los métodos de string que vas a usar constantemente al limpiar datos de formularios.

Número con rango (min/max)

Para campos como edad, cantidad de productos o cualquier valor numérico con límites, HTML5 ya hace la mayor parte del trabajo con type="number", min y max. El objeto validity te dice exactamente qué límite se rompió:

HTML 📋
<input type="number" id="edad" min="18" max="99" required>
JavaScript 📋
function validarEdad(input, spanError) {
    if (input.validity.valueMissing) {
        spanError.textContent = 'Este campo es obligatorio';
    } else if (input.validity.rangeUnderflow) {
        spanError.textContent = `El mínimo es ${input.min}`;
    } else if (input.validity.rangeOverflow) {
        spanError.textContent = `El máximo es ${input.max}`;
    } else {
        spanError.textContent = '';
    }
    return input.checkValidity();
}

Validar DNI español (y documentos similares)

Este caso no lo vas a encontrar en ningún tutorial en inglés, pero aparece constantemente en formularios para España. El DNI tiene 8 números y una letra de control, calculada con el resto de dividir esos 8 números entre 23:

JavaScript 📋
function validarDNI(input, spanError) {
    const dni    = input.value.trim().toUpperCase();
    const regex  = /^\d{8}[A-Z]$/;
    const letras = 'TRWAGMYFPDXBNJZSQVHLCKE';

    if (dni === '') {
        spanError.textContent = 'El DNI es obligatorio';
        return false;
    }
    if (!regex.test(dni)) {
        spanError.textContent = 'Formato incorrecto. Ejemplo: 12345678Z';
        return false;
    }

    const numero        = parseInt(dni.substring(0, 8), 10);
    const letraCorrecta = letras[numero % 23];

    if (dni.charAt(8) !== letraCorrecta) {
        spanError.textContent = `La letra no es correcta (debería ser ${letraCorrecta})`;
        return false;
    }

    spanError.textContent = '';
    return true;
}

El error que más veces he visto en proyectos reales es validar solo el formato (8 dígitos + una letra cualquiera) sin comprobar que la letra sea la correcta según el cálculo. La misma técnica, regex de formato más un dígito o letra de control calculado con una fórmula, sirve para otros documentos de identidad como el CURP en México, el RUT en Chile o el DNI en Perú; cada uno tiene su propia fórmula, así que no es la misma función, pero sí el mismo enfoque.

Mensajes de error accesibles

Un mensaje de error que solo se ve en rojo no sirve para nada si lo usa alguien con un lector de pantalla. Necesitas tres cosas en cada campo:

HTML 📋
<label for="email">Email</label>
<input type="email" id="email" aria-describedby="email-error" required>
<span id="email-error" class="fv-error" role="alert"></span>
JavaScript 📋
function marcarEstado(input, esValido) {
    input.setAttribute('aria-invalid', esValido ? 'false' : 'true');
    input.classList.toggle('fv-invalid', !esValido);
    input.classList.toggle('fv-valid', esValido);
}

Al enviar el formulario con errores, mueve el foco al primer campo que falla. Es la diferencia entre un usuario que sabe exactamente qué corregir y uno que se queda mirando la pantalla sin saber por qué no avanza:

JavaScript 📋
const primerCampoInvalido = formulario.querySelector('[aria-invalid="true"]');
if (primerCampoInvalido) {
    primerCampoInvalido.focus();
}

Evitar el envío con errores

El evento submit con preventDefault() es lo que te da el control: primero compruebas todo, y solo si todo pasa dejas que el formulario se envíe (o haces tú mismo el fetch, según el caso).

JavaScript 📋
formulario.addEventListener('submit', function(evento) {
    evento.preventDefault();

    const esValido = validarTodosLosCampos();

    if (esValido) {
        // aquí iría el envío real: fetch, o formulario.submit()
    }
});

El atributo novalidate en la etiqueta <form> desactiva la validación nativa del navegador por completo. Úsalo solo cuando ya tienes tu propia validación JavaScript cubriendo el 100% de los casos y quieres control total sobre cuándo y cómo se muestran los mensajes, incluyendo el primer intento de envío. Si no pones novalidate, el navegador puede mostrar su propio globo de error nativo antes de que tu JavaScript llegue a ejecutarse, lo que da una mezcla rara de dos estilos de error distintos en el mismo formulario.

Ejemplo completo para copiar

Todo lo anterior junto, en un formulario de registro con nombre, email, teléfono, contraseña y confirmación. Pruébalo aquí mismo: envíalo vacío, prueba un email sin arroba, un teléfono corto, contraseñas que no coincidan, y luego rellénalo todo bien.

Pruébalo:

  • Al menos 8 caracteres
  • Una mayúscula
  • Un número
Cuenta creada correctamente

El HTML completo de ese formulario (sin las clases de demo, para tu propio proyecto):

HTML 📋
<form id="registro-form" novalidate>
    <div class="form-group">
        <label for="nombre">Nombre</label>
        <input type="text" id="nombre" name="nombre" minlength="2" required
               aria-describedby="nombre-error">
        <span id="nombre-error" class="error-message" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" required
               aria-describedby="email-error">
        <span id="email-error" class="error-message" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="telefono">Teléfono</label>
        <input type="tel" id="telefono" name="telefono" required
               aria-describedby="telefono-error">
        <span id="telefono-error" class="error-message" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="password">Contraseña</label>
        <input type="password" id="password" name="password" minlength="8" required
               aria-describedby="password-rules password-error">
        <button type="button" id="toggle-password" aria-pressed="false">Mostrar</button>
        <ul id="password-rules">
            <li data-rule="length">Al menos 8 caracteres</li>
            <li data-rule="upper">Una mayúscula</li>
            <li data-rule="number">Un número</li>
        </ul>
        <span id="password-error" class="error-message" role="alert"></span>
    </div>

    <div class="form-group">
        <label for="confirmar">Confirmar contraseña</label>
        <input type="password" id="confirmar" name="confirmar" required
               aria-describedby="confirmar-error">
        <span id="confirmar-error" class="error-message" role="alert"></span>
    </div>

    <button type="submit">Crear cuenta</button>
    <div id="success-message" style="display:none;">Cuenta creada correctamente</div>
</form>

Y el JavaScript que conecta todo, listo para pegar tal cual (ajusta los id si en tu HTML usas otros):

registro-validacion.js 📋
document.addEventListener('DOMContentLoaded', function() {
    const formulario   = document.getElementById('registro-form');
    const campoNombre  = document.getElementById('nombre');
    const campoEmail   = document.getElementById('email');
    const campoTelefono = document.getElementById('telefono');
    const campoPassword = document.getElementById('password');
    const campoConfirmar = document.getElementById('confirmar');
    const botonMostrar  = document.getElementById('toggle-password');
    const reglasPassword = document.querySelectorAll('#password-rules li');

    function marcarEstado(input, esValido) {
        input.setAttribute('aria-invalid', esValido ? 'false' : 'true');
        input.classList.toggle('error', !esValido);
        input.classList.toggle('success', esValido);
    }

    function mostrarError(input, mensaje) {
        const errorEl = document.getElementById(input.id + '-error');
        errorEl.textContent = mensaje;
        marcarEstado(input, mensaje === '');
    }

    function validarNombre() {
        if (campoNombre.validity.valueMissing) {
            mostrarError(campoNombre, 'El nombre es obligatorio');
        } else if (campoNombre.validity.tooShort) {
            mostrarError(campoNombre, 'Al menos 2 caracteres');
        } else {
            mostrarError(campoNombre, '');
        }
        return campoNombre.checkValidity();
    }

    function validarEmail() {
        if (campoEmail.validity.valueMissing) {
            mostrarError(campoEmail, 'El email es obligatorio');
        } else if (campoEmail.validity.typeMismatch) {
            mostrarError(campoEmail, 'Escribe un email válido (ejemplo: nombre@dominio.com)');
        } else {
            mostrarError(campoEmail, '');
        }
        return campoEmail.checkValidity();
    }

    function validarTelefono() {
        const limpio = campoTelefono.value.replace(/[\s-]/g, '');
        const regex  = /^\+?[0-9]{7,15}$/;

        if (limpio === '') {
            mostrarError(campoTelefono, 'El teléfono es obligatorio');
            return false;
        }
        if (!regex.test(limpio)) {
            mostrarError(campoTelefono, 'Introduce un teléfono válido (7 a 15 dígitos)');
            return false;
        }
        mostrarError(campoTelefono, '');
        return true;
    }

    function actualizarReglasPassword() {
        const valor = campoPassword.value;
        const cumple = {
            length: valor.length >= 8,
            upper: /[A-Z]/.test(valor),
            number: /[0-9]/.test(valor)
        };

        reglasPassword.forEach(function(li) {
            li.classList.toggle('fv-rule-ok', cumple[li.dataset.rule]);
        });

        return Object.values(cumple).every(Boolean);
    }

    function validarPassword() {
        const todasCumplidas = actualizarReglasPassword();
        mostrarError(campoPassword, todasCumplidas ? '' : 'La contraseña no cumple los requisitos');
        return todasCumplidas;
    }

    campoPassword.addEventListener('input', actualizarReglasPassword);

    function validarConfirmacion() {
        if (campoConfirmar.value !== campoPassword.value || campoConfirmar.value === '') {
            mostrarError(campoConfirmar, 'Las contraseñas no coinciden');
            return false;
        }
        mostrarError(campoConfirmar, '');
        return true;
    }

    function activarEnTiempoReal(campo, validador) {
        let yaSeValido = false;
        campo.addEventListener('blur', function() {
            yaSeValido = true;
            validador();
        });
        campo.addEventListener('input', function() {
            if (yaSeValido) validador();
        });
    }

    activarEnTiempoReal(campoNombre, validarNombre);
    activarEnTiempoReal(campoEmail, validarEmail);
    activarEnTiempoReal(campoTelefono, validarTelefono);
    activarEnTiempoReal(campoPassword, validarPassword);
    activarEnTiempoReal(campoConfirmar, validarConfirmacion);
    campoPassword.addEventListener('input', validarConfirmacion);

    botonMostrar.addEventListener('click', function() {
        const oculto = campoPassword.type === 'password';
        campoPassword.type = oculto ? 'text' : 'password';
        botonMostrar.textContent = oculto ? 'Ocultar' : 'Mostrar';
        botonMostrar.setAttribute('aria-pressed', String(oculto));
    });

    formulario.addEventListener('submit', function(evento) {
        evento.preventDefault();

        const resultados = [
            validarNombre(),
            validarEmail(),
            validarTelefono(),
            validarPassword(),
            validarConfirmacion()
        ];

        const todoValido = resultados.every(Boolean);

        if (!todoValido) {
            const primerInvalido = formulario.querySelector('[aria-invalid="true"]');
            if (primerInvalido) primerInvalido.focus();
            return;
        }

        document.getElementById('success-message').style.display = 'block';
        formulario.reset();
        reglasPassword.forEach(function(li) { li.classList.remove('fv-rule-ok'); });
    });
});

Fíjate en el final del submit: cuando todo es válido, se muestra el mensaje de éxito y se llama a formulario.reset(). No hay ningún fetch ni petición de red en este ejemplo: el objetivo es que entiendas la validación en sí, no el envío. Cuando conectes esto a tu backend real, sustituye esas dos líneas por tu llamada a la API. Cómo hacerlo con fetch, JSON y los errores que devuelve el servidor lo tienes en enviar un formulario al backend con fetch.

La validación en el navegador no es seguridad

⚠️ Importante:

Todo lo que has visto en esta guía mejora la experiencia del usuario. No es seguridad. Cualquiera puede abrir las herramientas de desarrollador, desactivar JavaScript o mandar una petición directamente a tu servidor saltándose el formulario por completo. Si tu backend confía en que los datos ya vienen validados porque "el formulario los valida", tienes un problema.

La validación real, la que decide si un dato entra en tu base de datos, va siempre en el servidor. Si estás construyendo el backend con PHP, tenemos una guía completa de cómo crear un formulario de contacto con PHP y MySQL donde se valida cada campo también del lado del servidor antes de guardarlo.

Errores comunes

❌ Validar solo en el evento input desde el principio:

El usuario ve errores mientras todavía está escribiendo la primera letra del campo. Valida en blur primero y usa input solo para quitar el error una vez que ya se mostró.

❌ Olvidar aria-live o role="alert" en los mensajes de error:

Un usuario con lector de pantalla no se entera de que hay un error si el mensaje solo cambia visualmente. Sin esos atributos, el error existe en pantalla pero es invisible para quien no puede verla.

❌ Bloquear pegar en el campo de contraseña:

onpaste="return false" rompe los gestores de contraseñas y empuja a la gente a usar contraseñas más débiles y fáciles de recordar. No lo hagas nunca.

❌ Confiar solo en el cliente para seguridad:

JavaScript se puede desactivar o saltar por completo. La validación del navegador es para la experiencia de usuario, no un filtro de seguridad.

Preguntas frecuentes

¿Cómo validar un formulario sin jQuery?

Exactamente como se ha hecho en esta guía: con document.querySelector, addEventListener y la Constraint Validation API nativa del navegador. jQuery no aporta nada aquí que JavaScript moderno no tenga ya integrado, y añade una dependencia externa que no necesitas para validar campos.

¿Cuál es la diferencia entre blur e input para validar?

blur se dispara cuando el campo pierde el foco, es decir, cuando el usuario ya terminó de escribir ahí. input se dispara con cada tecla. Usa blur para la primera validación y input solo después, para que el error desaparezca al instante en cuanto se corrige.

¿Hace falta novalidate siempre?

No. Solo lo necesitas si tu JavaScript cubre el 100% de la validación y quieres controlar tú mismo cuándo aparece cada mensaje, incluyendo en el primer envío. Si tu validación JavaScript es parcial, deja que HTML5 se encargue del resto sin novalidate.

¿Por qué no basta con required?

required resuelve el caso de "campo vacío", pero no puedes personalizar el mensaje de forma consistente entre navegadores, no puedes mostrar reglas en tiempo real como las de una contraseña segura, y el mensaje nativo no llega a tu backend. Para validaciones más allá de "obligatorio" necesitas la Constraint Validation API o tu propia lógica.

¿Puedo usar regex para validar todo?

Puedes, pero no deberías para lo que HTML5 ya resuelve mejor. Usa type, pattern, min/max y minlength/maxlength primero, y añade regex solo para las reglas que de verdad no tienen un atributo nativo, como comparar dos campos entre sí o validar la letra de un documento de identidad.

Si necesitas añadir campos al formulario de forma dinámica (un botón "Añadir otro teléfono", por ejemplo), el tutorial de createElement te explica cómo crear esos elementos sin usar innerHTML.

👉 Formulario de contacto básico con HTML y CSS

El punto de partida si todavía no tienes el formulario montado.

No hemos podido validar tu suscripción.
¡Apuntado! Te avisaré cuando la guía esté lista.

¿Quieres crear tu primera app real?

Estoy preparando una guía completa de prompts para construir y desplegar apps con IA. Déjame tu email y te aviso cuando esté lista.

Usamos Brevo como plataforma de marketing. Al enviar este formulario, aceptas que los datos personales que proporcionaste se transferirán a Brevo para su procesamiento, de acuerdo con la Política de privacidad de Brevo.