Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Validate HTML against W3C standards

HTML markup is validated against W3C standards for cross-browser compatibility.

Utilities
Quick take
Typical fix time 15 min
  • Use validator.w3.org or browser extensions for validation
  • Fix errors first, then warnings (errors cause rendering issues)
  • Integrate validation into CI/CD with html-validate
  • Common issues: unclosed tags, invalid nesting, missing attributes
Why it matters: Invalid HTML causes unpredictable rendering across browsers, breaks accessibility tools, and makes debugging significantly harder.

Rule Details

W3C HTML validation ensures your markup follows web standards, improving cross-browser compatibility, accessibility, and code maintainability.

Code Examples

W3C Markup Validator

# Online validator
# Visit: https://validator.w3.org/
 
# Upload file or enter URL for validation
# Or paste HTML directly into the validator

Common Validation Errors and Fixes

<!-- ❌ Unclosed tags -->
<div>
  <p>Some content
  <span>More content
</div>
 
<!-- ✅ Properly closed tags -->
<div>
  <p>Some content</p>
  <span>More content</span>
</div>
 
<!-- ❌ Missing required attributes -->
<img src="image.jpg">
<input type="text">
 
<!-- ✅ Required attributes included -->
<img src="image.jpg" alt="Description">
<input type="text" name="username" id="username">
 
<!-- ❌ Invalid nesting -->
<p>
  <div>Block element inside paragraph</div>
</p>
 
<!-- ✅ Proper nesting -->
<div>
  <p>Paragraph content</p>
  <div>Block element content</div>
</div>
 
<!-- ❌ Duplicate IDs -->
<div id="content">First div</div>
<div id="content">Second div</div>
 
<!-- ✅ Unique IDs -->
<div id="main-content">First div</div>
<div id="sidebar-content">Second div</div>
 
<!-- ❌ Missing DOCTYPE -->
<html>
<head><title>Page</title></head>
 
<!-- ✅ Proper DOCTYPE -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Page</title>
</head>

Why It Matters

Invalid HTML causes unpredictable rendering across browsers, breaks accessibility tools, and makes debugging significantly harder.

Automated Validation

HTML Validator CLI

# Install html-validate
npm install -g html-validate
 
# Validate single file
html-validate index.html
 
# Validate directory
html-validate src/**/*.html
 
# With configuration file
html-validate --config .htmlvalidate.json src/

Configuration File (.htmlvalidate.json)

{
  "extends": ["html-validate:recommended"],
  "rules": {
    "void-style": ["error", "omit"],
    "close-order": "error",
    "doctype-html": "error",
    "no-missing-references": "error",
    "require-sri": "error",
    "no-inline-style": "warn",
    "element-required-attributes": "error",
    "element-permitted-content": "error",
    "element-permitted-parent": "error"
  },
  "elements": [
    "html5"
  ]
}

Build Tool Integration

Webpack Plugin

// webpack.config.js
const HtmlValidatePlugin = require('html-validate/webpack')
 
module.exports = {
  plugins: [
    new HtmlValidatePlugin({
      configFile: '.htmlvalidate.json'
    })
  ]
}

Gulp Task

// gulpfile.js
const gulp = require('gulp')
const htmlValidator = require('gulp-html-validator')
 
gulp.task('validate-html', () => {
  return gulp.src('dist/**/*.html')
    .pipe(htmlValidator({
      format: 'json',
      validator: 'https://validator.w3.org/nu/'
    }))
    .pipe(gulp.dest('reports/'))
})

Grunt Task

// Gruntfile.js
module.exports = function(grunt) {
  grunt.initConfig({
    htmllint: {
      all: {
        options: {
          ignore: [
            'Bad value "X-UA-Compatible" for attribute "http-equiv" on element "meta".'
          ]
        },
        src: ['dist/**/*.html']
      }
    }
  })
 
  grunt.loadNpmTasks('grunt-htmllint')
  grunt.registerTask('validate', ['htmllint'])
}

Framework-Specific Validation

React JSX Validation

// .eslintrc.js
module.exports = {
  extends: ['plugin:react/recommended'],
  plugins: ['react', 'jsx-a11y'],
  rules: {
    'react/jsx-no-duplicate-props': 'error',
    'react/jsx-no-undef': 'error',
    'react/jsx-uses-vars': 'error',
    'react/no-unescaped-entities': 'error',
    'react/self-closing-comp': 'error',
    'jsx-a11y/alt-text': 'error',
    'jsx-a11y/img-redundant-alt': 'error'
  }
}
 
// Custom validation component
import React from 'react'
import PropTypes from 'prop-types'
 
function ValidatedImage({ src, alt, ...props }) {
  if (!alt) {
    console.error('ValidatedImage: alt attribute is required for accessibility')
  }
 
  if (!src) {
    console.error('ValidatedImage: src attribute is required')
  }
 
  return <img src={src} alt={alt} {...props} />
}
 
ValidatedImage.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired
}
 
export default ValidatedImage

Next.js HTML Validation

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')
 
module.exports = withBundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
 
  // Enable strict mode for better HTML validation
  reactStrictMode: true,
 
  // Custom webpack config for HTML validation
  webpack: (config, { dev, isServer }) => {
    if (dev && !isServer) {
      config.plugins.push(
        new (require('html-validate/webpack'))({
          configFile: '.htmlvalidate.json'
        })
      )
    }
    return config
  },
 
  // Experimental features for better validation
  experimental: {
    strictNextHead: true
  }
})
 
// pages/_document.js - Ensure valid HTML structure
import { Html, Head, Main, NextScript } from 'next/document'
 
export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <meta charSet="utf-8" />
        {/* Ensure proper meta tags */}
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

Vue.js Template Validation

// vue.config.js
module.exports = {
  chainWebpack: config => {
    if (process.env.NODE_ENV === 'development') {
      config.plugin('html-validate')
        .use(require('html-validate/webpack'), [{
          configFile: '.htmlvalidate.json'
        }])
    }
  }
}
 
// .eslintrc.js for Vue
module.exports = {
  extends: [
    'plugin:vue/vue3-essential',
    'plugin:vue/vue3-strongly-recommended'
  ],
  rules: {
    'vue/html-self-closing': ['error', {
      'html': {
        'void': 'always',
        'normal': 'never',
        'component': 'always'
      }
    }],
    'vue/max-attributes-per-line': ['error', {
      'singleline': 3,
      'multiline': 1
    }],
    'vue/require-v-for-key': 'error',
    'vue/no-duplicate-attributes': 'error'
  }
}

CI/CD Integration

GitHub Actions

# .github/workflows/html-validation.yml
name: HTML Validation
 
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
 
jobs:
  validate-html:
    runs-on: ubuntu-latest
 
    steps:
    - uses: actions/checkout@v4
 
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
 
    - name: Install dependencies
      run: npm ci
 
    - name: Build project
      run: npm run build
 
    - name: Validate HTML
      run: |
        npm install -g html-validate
        html-validate dist/**/*.html
 
    - name: W3C Validation
      uses: Cyb3r-Jak3/html5validator-action@v7.2.0
      with:
        root: dist/
        css: true
 
    - name: Upload validation report
      uses: actions/upload-artifact@v3
      if: failure()
      with:
        name: html-validation-report
        path: validation-report.json

GitLab CI

# .gitlab-ci.yml
stages:
  - build
  - validate
 
build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
 
validate-html:
  stage: validate
  dependencies:
    - build
  script:
    - npm install -g html-validate
    - html-validate dist/**/*.html
    - |
      curl -H "Content-Type: text/html; charset=utf-8" \
           --data-binary @dist/index.html \
           https://validator.w3.org/nu/?out=json
  artifacts:
    reports:
      junit: validation-report.xml

Pre-commit Hooks

# Install pre-commit
pip install pre-commit
 
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: html-validate
        name: HTML Validate
        entry: html-validate
        language: node
        files: \.(html)$
        additional_dependencies: [html-validate]
 
      - id: w3c-validate
        name: W3C HTML Validator
        entry: bash -c 'for file in "$@"; do curl -H "Content-Type: text/html; charset=utf-8" --data-binary "@$file" "https://validator.w3.org/nu/?out=text" | grep -q "Error" && exit 1; done' --
        language: system
        files: \.(html)$
 
# Install hooks
pre-commit install

Custom Validation Rules

Build Custom Validator

// custom-html-validator.js
const fs = require('fs')
const path = require('path')
const cheerio = require('cheerio')
 
class CustomHTMLValidator {
  constructor(options = {}) {
    this.rules = options.rules || []
    this.errors = []
    this.warnings = []
  }
 
  addRule(rule) {
    this.rules.push(rule)
  }
 
  validateFile(filePath) {
    const content = fs.readFileSync(filePath, 'utf8')
    const $ = cheerio.load(content)
 
    this.rules.forEach(rule => {
      try {
        rule.validate($, filePath, this)
      } catch (error) {
        this.addError(`Rule "${rule.name}" failed: ${error.message}`)
      }
    })
 
    return {
      errors: this.errors,
      warnings: this.warnings,
      isValid: this.errors.length === 0
    }
  }
 
  addError(message, line = null, column = null) {
    this.errors.push({ message, line, column, type: 'error' })
  }
 
  addWarning(message, line = null, column = null) {
    this.warnings.push({ message, line, column, type: 'warning' })
  }
}
 
// Custom validation rules
const requireAltText = {
  name: 'require-alt-text',
  validate($, filePath, validator) {
    $('img').each((i, img) => {
      const $img = $(img)
      if (!$img.attr('alt')) {
        validator.addError(`Image missing alt attribute in ${filePath}`)
      }
    })
  }
}
 
const requireLangAttribute = {
  name: 'require-lang',
  validate($, filePath, validator) {
    if (!$('html').attr('lang')) {
      validator.addError(`HTML element missing lang attribute in ${filePath}`)
    }
  }
}
 
const noInlineStyles = {
  name: 'no-inline-styles',
  validate($, filePath, validator) {
    $('[style]').each((i, element) => {
      validator.addWarning(`Inline styles found in ${filePath}`)
    })
  }
}
 
// Usage
const validator = new CustomHTMLValidator()
validator.addRule(requireAltText)
validator.addRule(requireLangAttribute)
validator.addRule(noInlineStyles)
 
const result = validator.validateFile('dist/index.html')
console.log(result)

Integration with Testing Frameworks

Jest Integration

// __tests__/html-validation.test.js
const fs = require('fs')
const path = require('path')
const { JSDOM } = require('jsdom')
 
describe('HTML Validation', () => {
  const htmlFiles = fs.readdirSync('dist')
    .filter(file => file.endsWith('.html'))
    .map(file => path.join('dist', file))
 
  test.each(htmlFiles)('should be valid HTML: %s', (filePath) => {
    const content = fs.readFileSync(filePath, 'utf8')
    const dom = new JSDOM(content)
    const document = dom.window.document
 
    // Test for required elements
    expect(document.doctype).toBeTruthy()
    expect(document.querySelector('html')).toBeTruthy()
    expect(document.querySelector('head')).toBeTruthy()
    expect(document.querySelector('body')).toBeTruthy()
 
    // Test for lang attribute
    const htmlElement = document.querySelector('html')
    expect(htmlElement.getAttribute('lang')).toBeTruthy()
 
    // Test for meta charset
    const charset = document.querySelector('meta[charset]')
    expect(charset).toBeTruthy()
 
    // Test all images have alt attributes
    const images = document.querySelectorAll('img')
    images.forEach(img => {
      expect(img.getAttribute('alt')).not.toBeNull()
    })
 
    // Test for unique IDs
    const elementsWithIds = document.querySelectorAll('[id]')
    const ids = Array.from(elementsWithIds).map(el => el.id)
    const uniqueIds = [...new Set(ids)]
    expect(ids.length).toBe(uniqueIds.length)
  })
})

Cypress Integration

// cypress/e2e/html-validation.cy.js
describe('HTML Validation', () => {
  it('should have valid HTML structure', () => {
    cy.visit('/')
 
    // Check for proper DOCTYPE
    cy.document().should('have.property', 'doctype')
 
    // Check for lang attribute
    cy.get('html').should('have.attr', 'lang')
 
    // Check for charset meta tag
    cy.get('meta[charset]').should('exist')
 
    // Check all images have alt text
    cy.get('img').each(($img) => {
      cy.wrap($img).should('have.attr', 'alt')
    })
 
    // Check for unique IDs
    cy.get('[id]').then(($elements) => {
      const ids = $elements.toArray().map(el => el.id)
      const uniqueIds = [...new Set(ids)]
      expect(ids.length).to.equal(uniqueIds.length)
    })
  })
 
  it('should pass W3C validation', () => {
    cy.visit('/')
 
    cy.get('html').then(($html) => {
      const htmlContent = $html[0].outerHTML
 
      // Send to W3C validator
      cy.request({
        method: 'POST',
        url: 'https://validator.w3.org/nu/?out=json',
        headers: {
          'Content-Type': 'text/html; charset=utf-8'
        },
        body: htmlContent
      }).then((response) => {
        const errors = response.body.messages.filter(msg => msg.type === 'error')
        expect(errors).to.have.length(0)
      })
    })
  })
})

Performance Impact of Valid HTML

Rendering Performance

<!-- ❌ Invalid HTML can cause parsing issues -->
<div>
  <p>Unclosed paragraph
  <span>Nested content
    <div>Block in inline</div>
</div>
 
<!-- ✅ Valid HTML ensures predictable parsing -->
<div>
  <p>Properly closed paragraph</p>
  <div>
    <span>Properly nested content</span>
  </div>
</div>

Browser Compatibility

// Monitor parsing errors in production
window.addEventListener('error', (event) => {
  if (event.target.tagName) {
    console.error('HTML parsing error:', {
      element: event.target.tagName,
      source: event.target.src || event.target.href,
      message: event.message
    })
 
    // Send to analytics
    analytics.track('html_parsing_error', {
      element: event.target.tagName,
      url: window.location.href
    })
  }
})

Best Practices

  1. Validate early and often: Include validation in development workflow
  2. Automate validation: Use build tools and CI/CD pipelines
  3. Fix errors immediately: Don't accumulate validation debt
  4. Use semantic HTML: Proper structure improves validation success
  5. Test across browsers: Valid HTML ensures better compatibility
  6. Monitor in production: Track HTML-related errors
  7. Educate team: Ensure all developers understand HTML standards
  8. Document exceptions: When breaking standards, document why

Common Validation Errors

  1. Unclosed tags - Always close HTML tags properly
  2. Missing alt attributes - Required for accessibility
  3. Duplicate IDs - Must be unique within the document
  4. Invalid nesting - Block elements can't go inside inline elements
  5. Missing DOCTYPE - Required for standards mode
  6. Missing lang attribute - Important for accessibility
  7. Obsolete elements - Use modern semantic elements
  8. Invalid attributes - Check attribute names and values

Standards

  • Use MDN: HTML as the standard for the final rendered HTML and browser-facing behavior.
  • Use WHATWG HTML Living Standard as the standard for the final rendered HTML and browser-facing behavior.

Verification

Automated Checks

  • Inspect the final rendered HTML in the browser or page source to confirm the rule is satisfied.
  • Validate the affected markup with browser tooling or an HTML validator where appropriate.
  • Test one representative route or template that uses the pattern.
  • Re-check shared components that emit the same markup so the fix is consistent.

Manual Checks

  • Verify the rendered browser behavior manually on representative routes and supported browsers so the user-facing outcome matches the rule.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Validate HTML markup using the W3C HTML Validator to identify and fix markup errors that could cause cross-browser compatibility issues.

Fix

Auto-fix issues

Run HTML through W3C validator and fix reported errors including unclosed tags, missing attributes, and invalid nesting.

Explain

Learn more

Explain why W3C HTML validation is important for cross-browser compatibility, accessibility, and maintainable code.

Review

Code review

Review templates, server-rendered HTML, and shared components that output markup related to Validate HTML against W3C standards. Flag exact elements, attributes, and routes where the rendered HTML violates the rule.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Nu Html Checker
validator.w3.orgTool

Rules that often go hand-in-hand with this one.

Remove comments and debug code in production

Unnecessary code, comments, and debug elements are removed before deploying to production.

HTML
Validate forms accessibly

Forms provide clear validation feedback with accessible error messages and proper ARIA attributes.

HTML
Set text direction for RTL languages

The dir attribute is used for languages that read right-to-left (RTL) or mixed content.

HTML
Lint CSS and SCSS files

All CSS/SCSS files are linted with Stylelint to detect errors and enforce standards.

CSS

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385