React Form Validation Guide for Beginners

Faraz

By Faraz - Last Updated:

Learn how to create and validate forms in React using Formik and Yup. Simple guide with setup, code, and live form validation example.


react-form-validation-guide-for-beginners.webp

Forms are a big part of web apps. Whether it’s a login form, a signup form, or a contact form, users need to enter data, and that data must be correct. This is why form validation is important.

Creating forms in React is common, but validating them is just as important. Bad user input can break your app or result in errors. That’s why form validation is a must.

In this guide, we will show you how to build a form in React using Formik and Yup. Formik helps handle form state, and Yup is used for easy validation rules. Together, they make form handling easy and clean — even for beginners.

Setup Environment for React Form Validation

Follow these simple steps to set up your project:

Step 1: Install Node.js and npm

Make sure Node.js is installed on your system. You can check it by running:

node -v
npm -v

If not installed, download it from https://nodejs.org.

Step 2: Create React App using Vite (Fast Setup)

Open terminal and run:

npm create vite@latest react-form-validate --template react

Follow the prompts:

  • Select React as framework
  • Choose JavaScript as variant

Then move to your project folder:

cd react-form-validate
npm install

Run the development server:

npm run dev

Now visit http://localhost:5173 in your browser.

Step 3: Install Formik and Yup

Install both libraries with npm:

npm install formik yup

Step-by-Step: Build React Form

Step 1: Replace src/App.jsx Code

import { useFormik } from 'formik';
import * as Yup from 'yup';
import './App.css';

const countries = [
  { value: '', label: 'Select a country' },
  { value: 'us', label: 'United States' },
  { value: 'ca', label: 'Canada' },
  { value: 'uk', label: 'United Kingdom' },
  { value: 'au', label: 'Australia' },
  { value: 'de', label: 'Germany' },
  { value: 'fr', label: 'France' },
  { value: 'jp', label: 'Japan' },
];

const AdvancedForm = () => {
  const formik = useFormik({
    initialValues: {
      fullName: '',
      username: '',
      email: '',
      phone: '',
      dob: '',
      gender: '',
      country: '',
      password: '',
      confirmPassword: '',
      terms: false,
    },
    validationSchema: Yup.object().shape({
      fullName: Yup.string().required('Full name is required'),
      username: Yup.string()
        .min(4, 'Username must be at least 4 characters')
        .required('Username is required'),
      email: Yup.string()
        .email('Invalid email format')
        .required('Email is required'),
      phone: Yup.string()
        .matches(/^[0-9]+$/, 'Phone number must contain only digits')
        .min(10, 'Phone number must be at least 10 digits')
        .max(15, 'Phone number must not exceed 15 digits')
        .required('Phone number is required'),
      dob: Yup.date()
        .max(new Date(), 'Date of birth cannot be in the future')
        .required('Date of birth is required'),
      gender: Yup.string().required('Gender is required'),
      country: Yup.string().required('Country is required'),
      password: Yup.string()
        .min(8, 'Password must be at least 8 characters')
        .matches(/[a-zA-Z]/, 'Password must contain at least one letter')
        .matches(/[0-9]/, 'Password must contain at least one number')
        .required('Password is required'),
      confirmPassword: Yup.string()
        .oneOf([Yup.ref('password'), null], 'Passwords must match')
        .required('Please confirm your password'),
      terms: Yup.boolean()
        .oneOf([true], 'You must accept the terms and conditions')
        .required('You must accept the terms and conditions'),
    }),
    onSubmit: (values) => {
      // Simulate form submission
      return new Promise(resolve => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          resolve();
        }, 1500);
      });
    },
  });

  return (
    <div className="advanced-form-container">
      <div className="form-header">
        <div className="form-icon">
          <svg viewBox="0 0 24 24">
            <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
          </svg>
        </div>
        <h1 className="form-title">Create Account</h1>
        <p className="form-subtitle">Join our community today</p>
      </div>

      <form className="advanced-form" onSubmit={formik.handleSubmit}>
        <div className="form-grid">
          <div className={`form-group ${formik.touched.fullName && formik.errors.fullName ? 'has-error' : ''}`}>
            <label htmlFor="fullName" className="form-label">
              <span>Full Name</span>
              <input
                id="fullName"
                name="fullName"
                type="text"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.fullName}
              />
            </label>
            {formik.touched.fullName && formik.errors.fullName && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.fullName}
              </div>
            )}
          </div>

          <div className={`form-group ${formik.touched.username && formik.errors.username ? 'has-error' : ''}`}>
            <label htmlFor="username" className="form-label">
              <span>Username</span>
              <input
                id="username"
                name="username"
                type="text"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.username}
              />
            </label>
            {formik.touched.username && formik.errors.username && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.username}
              </div>
            )}
          </div>

          <div className={`form-group ${formik.touched.email && formik.errors.email ? 'has-error' : ''}`}>
            <label htmlFor="email" className="form-label">
              <span>Email Address</span>
              <input
                id="email"
                name="email"
                type="email"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.email}
              />
            </label>
            {formik.touched.email && formik.errors.email && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.email}
              </div>
            )}
          </div>

          <div className={`form-group ${formik.touched.phone && formik.errors.phone ? 'has-error' : ''}`}>
            <label htmlFor="phone" className="form-label">
              <span>Phone Number</span>
              <input
                id="phone"
                name="phone"
                type="tel"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.phone}
              />
            </label>
            {formik.touched.phone && formik.errors.phone && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.phone}
              </div>
            )}
          </div>

          <div className={`form-group ${formik.touched.dob && formik.errors.dob ? 'has-error' : ''}`}>
            <label htmlFor="dob" className="form-label">
              <span>Date of Birth</span>
              <input
                id="dob"
                name="dob"
                type="date"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.dob}
              />
            </label>
            {formik.touched.dob && formik.errors.dob && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.dob}
              </div>
            )}
          </div>

          <div className={`form-group ${formik.touched.gender && formik.errors.gender ? 'has-error' : ''}`}>
            <label className="form-label">
              <span>Gender</span>
              <div className="radio-group">
                <label className="radio-option">
                  <input
                    type="radio"
                    name="gender"
                    value="male"
                    onChange={formik.handleChange}
                    onBlur={formik.handleBlur}
                    checked={formik.values.gender === 'male'}
                  />
                  <span className="radio-custom"></span>
                  <span>Male</span>
                </label>
                <label className="radio-option">
                  <input
                    type="radio"
                    name="gender"
                    value="female"
                    onChange={formik.handleChange}
                    onBlur={formik.handleBlur}
                    checked={formik.values.gender === 'female'}
                  />
                  <span className="radio-custom"></span>
                  <span>Female</span>
                </label>
                <label className="radio-option">
                  <input
                    type="radio"
                    name="gender"
                    value="other"
                    onChange={formik.handleChange}
                    onBlur={formik.handleBlur}
                    checked={formik.values.gender === 'other'}
                  />
                  <span className="radio-custom"></span>
                  <span>Other</span>
                </label>
              </div>
            </label>
            {formik.touched.gender && formik.errors.gender && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.gender}
              </div>
            )}
          </div>

          <div className={`form-group ${formik.touched.country && formik.errors.country ? 'has-error' : ''}`}>
            <label htmlFor="country" className="form-label">
              <span>Country</span>
              <div className="select-wrapper">
                <select
                  id="country"
                  name="country"
                  onChange={formik.handleChange}
                  onBlur={formik.handleBlur}
                  value={formik.values.country}
                >
                  {countries.map((country) => (
                    <option key={country.value} value={country.value}>
                      {country.label}
                    </option>
                  ))}
                </select>
              </div>
            </label>
            {formik.touched.country && formik.errors.country && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.country}
              </div>
            )}
          </div>

          <div className={`form-group ${formik.touched.password && formik.errors.password ? 'has-error' : ''}`}>
            <label htmlFor="password" className="form-label">
              <span>Password</span>
              <input
                id="password"
                name="password"
                type="password"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.password}
              />
            </label>
            {formik.touched.password && formik.errors.password && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.password}
              </div>
            )}
          </div>
          <div className={`form-group ${formik.touched.confirmPassword && formik.errors.confirmPassword ? 'has-error' : ''}`}>
            <label htmlFor="confirmPassword" className="form-label">
              <span>Confirm Password</span>
              <input
                id="confirmPassword"
                name="confirmPassword"
                type="password"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.confirmPassword}
              />
            </label>
            {formik.touched.confirmPassword && formik.errors.confirmPassword && (
              <div className="error-message">
                <svg viewBox="0 0 24 24">
                  <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
                </svg>
                {formik.errors.confirmPassword}
              </div>
            )}
          </div>
        </div>

        <div className={`form-group terms-group ${formik.touched.terms && formik.errors.terms ? 'has-error' : ''}`}>
          <label className="checkbox-option">
            <input
              type="checkbox"
              name="terms"
              onChange={formik.handleChange}
              onBlur={formik.handleBlur}
              checked={formik.values.terms}
            />
            <span className="checkbox-custom"></span>
            <span>I agree to the <a href="#">Terms & Conditions</a> and <a href="#">Privacy Policy</a></span>
          </label>
          {formik.touched.terms && formik.errors.terms && (
            <div className="error-message">
              <svg viewBox="0 0 24 24">
                <path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
              </svg>
              {formik.errors.terms}
            </div>
          )}
        </div>

        <button
          type="submit"
          className="submit-button"
          disabled={!formik.isValid || formik.isSubmitting}
        >
          {formik.isSubmitting ? (
            <span className="spinner"></span>
          ) : (
            'Create Account'
          )}
        </button>
        <div className="form-footer">
          Already have an account? <a href="#">Sign In</a>
        </div>
      </form>
    </div>
  );
};

export default AdvancedForm;

Step 2: Add Styling (CSS)

Edit src/App.css:

:root {
  --primary-color: #4361ee;
  --primary-hover: #3a56d4;
  --error-color: #f72585;
  --text-color: #2b2d42;
  --text-light: #8d99ae;
  --border-color: #b4b4b4;
  --bg-color: #ffffff;
  --shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
  --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  background-color: #f8f9fa;
  color: var(--text-color);
  line-height: 1.6;
}

.advanced-form-container {
  max-width: 800px;
  margin: 2rem auto;
  background: var(--bg-color);
  border-radius: 16px;
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: var(--transition);
}

.form-header {
  padding: 2.5rem 2.5rem 1.5rem;
  text-align: center;
  background: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
  color: white;
}

.form-icon {
  width: 60px;
  height: 60px;
  margin: 0 auto 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 50%;
  backdrop-filter: blur(5px);
}

.form-icon svg {
  width: 30px;
  height: 30px;
  fill: white;
}

.form-title {
  font-size: 1.8rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.form-subtitle {
  font-size: 0.95rem;
  opacity: 0.9;
  font-weight: 400;
}

.advanced-form {
  padding: 2rem 2.5rem;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.5rem;
  margin-bottom: 1.5rem;
}

.form-group {
  position: relative;
  margin-bottom: 0.5rem;
}

.form-label {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-color);
}

.form-label span {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-color);
}

.form-group input,
.form-group select {
  width: 100%;
  padding: 0.85rem 1rem;
  font-size: 0.95rem;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background-color: var(--bg-color);
  transition: var(--transition);
  appearance: none;
}

.form-group input:focus,
.form-group select:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
}

.form-group.has-error input,
.form-group.has-error select {
  border-color: var(--error-color);
}

.form-group.has-error input:focus,
.form-group.has-error select:focus {
  box-shadow: 0 0 0 3px rgba(247, 37, 133, 0.2);
}

.error-message {
  display: flex;
  align-items: center;
  margin-top: 0.5rem;
  font-size: 0.8rem;
  color: var(--error-color);
}

.error-message svg {
  width: 16px;
  height: 16px;
  margin-right: 0.5rem;
  fill: var(--error-color);
}

.radio-group {
  display: flex;
  gap: 1.5rem;
  margin-top: 0.5rem;
}

.radio-option {
  display: flex;
  align-items: center;
  cursor: pointer;
  font-size: 0.9rem;
  color: var(--text-color);
  position: relative;
}

.radio-option input {
  position: absolute;
  opacity: 0;
  cursor: pointer;
}

.radio-custom {
  position: relative;
  display: inline-block;
  width: 18px;
  height: 18px;
  margin-right: 0.75rem;
  border: 1px solid var(--border-color);
  border-radius: 50%;
  transition: var(--transition);
}

.radio-option input:checked ~ .radio-custom {
  border-color: var(--primary-color);
}

.radio-custom::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0);
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: var(--primary-color);
  transition: var(--transition);
}

.radio-option input:checked ~ .radio-custom::after {
  transform: translate(-50%, -50%) scale(1);
}

.select-wrapper {
  position: relative;
}

.select-wrapper::after {
  content: '';
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%);
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid var(--text-light);
  pointer-events: none;
}

.form-group select {
  padding-right: 2.5rem;
}

.checkbox-option {
  display: flex;
  align-items: center;
  cursor: pointer;
  font-size: 0.9rem;
  color: var(--text-color);
  position: relative;
}

.checkbox-option input {
  position: absolute;
  opacity: 0;
  cursor: pointer;
}

.checkbox-custom {
  position: relative;
  display: inline-block;
  width: 18px;
  height: 18px;
  margin-right: 0.75rem;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  transition: var(--transition);
}

.checkbox-option input:checked ~ .checkbox-custom {
  background-color: var(--primary-color);
  border-color: var(--primary-color);
}

.checkbox-custom::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0);
  width: 10px;
  height: 10px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  transition: var(--transition);
}

.checkbox-option input:checked ~ .checkbox-custom::after {
  transform: translate(-50%, -50%) scale(1);
}

.terms-group {
  margin: 1.5rem 0;
}

.terms-group a {
  color: var(--primary-color);
  text-decoration: none;
  font-weight: 500;
}

.terms-group a:hover {
  text-decoration: underline;
}

.submit-button {
  width: 100%;
  padding: 1rem;
  font-size: 1rem;
  font-weight: 600;
  color: white;
  background-color: var(--primary-color);
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: var(--transition);
  display: flex;
  align-items: center;
  justify-content: center;
}

.submit-button:hover {
  background-color: var(--primary-hover);
  transform: translateY(-2px);
}

.submit-button:disabled {
  background-color: #adb5bd;
  cursor: not-allowed;
  transform: none;
}

.submit-button:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.3);
}

.spinner {
  width: 20px;
  height: 20px;
  border: 3px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  border-top-color: white;
  animation: spin 1s ease-in-out infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.form-footer {
  text-align: center;
  margin-top: 1.5rem;
  font-size: 0.9rem;
  color: var(--text-light);
}

.form-footer a {
  color: var(--primary-color);
  text-decoration: none;
  font-weight: 500;
}

.form-footer a:hover {
  text-decoration: underline;
}

/* Responsive styles */
@media (max-width: 768px) {
  .form-grid {
    grid-template-columns: 1fr;
  }
  
  .advanced-form-container {
    margin: 1rem;
    border-radius: 12px;
  }
  
  .form-header {
    padding: 1.5rem 1.5rem 1rem;
  }
  
  .advanced-form {
    padding: 1.5rem;
  }
}

@media (max-width: 480px) {
  .radio-group {
    flex-direction: column;
    gap: 0.75rem;
  }
  
  .form-title {
    font-size: 1.5rem;
  }
}

Run Your App

Go to your terminal:

npm run dev

Open http://localhost:5173/ and see your form.

Conclusion

Using Formik with Yup makes React form validation easy and clean. You don’t need to write complex logic or track every input manually. With just a few lines of code, you can create fully working and validated forms.

Quick Recap:

  • Use Formik to handle form state
  • Use Yup to define validation rules
  • Clean code and real-time error messages
  • Simple for both small and large forms

Whether you're building login forms, signup pages, or contact forms, this method is the best way to get started.

Form Validation Live Demo ⟶

That’s a wrap!

I hope you enjoyed this article

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee.

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Components

Please allow ads on our site🥺