Ship internal tools in hours, not weeks. Real auth, users, jobs, audit logs, and cohesive UI included. Early access $249 $499 → [Get it now]

How to check if a string is a palindrome in JavaScript

Checking if strings are palindromes is useful for input validation, algorithm challenges, pattern detection, and implementing features like password symmetry warnings or data integrity checks in JavaScript applications. With over 25 years of experience in software development and as the creator of CoreUI, I’ve implemented palindrome checking in form validators and data processing utilities where detecting symmetric text patterns improves data quality. From my extensive expertise, the most intuitive and readable solution is comparing the string with its reversed version after normalizing the text. This approach is clear, handles case sensitivity and spaces appropriately, and provides reliable palindrome detection.

Use string reversal and comparison to check if a string is a palindrome.

const text = 'racecar'
const cleaned = text.toLowerCase().replace(/[^a-z0-9]/g, '')
const isPalindrome = cleaned === [...cleaned].reverse().join('')
// Result: true

This method first normalizes the string by converting to lowercase and removing non-alphanumeric characters to handle cases like 'A man, a plan, a canal: Panama'. Then it compares the cleaned string with its reversed version created by spreading into an array, reversing, and joining back. In this example, 'racecar' remains 'racecar' when reversed, so the comparison returns true. The cleaning step ensures that spaces, punctuation, and capitalization don’t affect the palindrome check, focusing only on the meaningful character sequence. We use the spread operator ([...cleaned]) instead of split('') because it correctly handles Unicode surrogate pairs like emoji.

1. Basic Palindrome Function

Wrapping the logic in a reusable function makes it easy to call from validators, filters, or test suites. The function should handle the most common real-world inputs: mixed case, spaces, and punctuation.

const isPalindrome = (text) => {
  const cleaned = text.toLowerCase().replace(/[^a-z0-9]/g, '')
  return cleaned === [...cleaned].reverse().join('')
}

console.log(isPalindrome('racecar'))
// Result: true

console.log(isPalindrome('hello'))
// Result: false

console.log(isPalindrome('A man, a plan, a canal: Panama'))
// Result: true

console.log(isPalindrome('Was it a car or a cat I saw?'))
// Result: true

console.log(isPalindrome('No lemon, no melon'))
// Result: true

// Edge cases
console.log(isPalindrome(''))
// Result: true (empty string is a palindrome by convention)

console.log(isPalindrome('a'))
// Result: true (single character)

console.log(isPalindrome('ab'))
// Result: false

The replace(/[^a-z0-9]/g, '') regex strips everything except lowercase letters and digits after the toLowerCase() call. This ensures that punctuation and spaces are ignored — the standard definition of a palindrome in algorithm challenges. For more on string reversal, see how to reverse a string in JavaScript.

2. Two-Pointer Approach for Better Performance

The reversal method creates a new array and string, using O(n) extra memory. For very long strings, a two-pointer approach is more efficient — it compares characters from both ends moving inward and can exit early on the first mismatch.

const isPalindromeFast = (text) => {
  const cleaned = text.toLowerCase().replace(/[^a-z0-9]/g, '')
  let left = 0
  let right = cleaned.length - 1

  while (left < right) {
    if (cleaned[left] !== cleaned[right]) {
      return false
    }
    left++
    right--
  }
  return true
}

console.log(isPalindromeFast('racecar'))
// Result: true

console.log(isPalindromeFast('hello'))
// Result: false (exits early at h !== o)

// Performance comparison
const longPalindrome = 'a'.repeat(100000) + 'b' + 'a'.repeat(100000)

console.time('reverse')
isPalindrome(longPalindrome)
console.timeEnd('reverse')
// ~15ms (must reverse entire string)

console.time('two-pointer')
isPalindromeFast(longPalindrome)
console.timeEnd('two-pointer')
// ~8ms (exits early, no extra allocation)

The two-pointer approach has the same O(n) time complexity in the worst case, but performs better in practice because it avoids creating a reversed copy and can return false as soon as it finds a mismatch. Use this for performance-critical code or when checking very long strings.

3. Handling Unicode and International Characters

The basic /[^a-z0-9]/g regex only keeps ASCII letters and digits, stripping accented characters like é, ñ, and ü. For international palindromes, use Unicode property escapes.

const isPalindromeUnicode = (text) => {
  // Keep all Unicode letters and numbers, remove everything else
  const cleaned = text.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '')
  return cleaned === [...cleaned].reverse().join('')
}

// French palindrome
console.log(isPalindromeUnicode('Été'))
// Result: true

// Works with accented characters
console.log(isPalindromeUnicode('Àlà'))
// Result: true

// ASCII-only version would fail these
const isPalindromeAscii = (text) => {
  const cleaned = text.toLowerCase().replace(/[^a-z0-9]/g, '')
  return cleaned === [...cleaned].reverse().join('')
}

console.log(isPalindromeAscii('Été'))
// Result: true — but only because 'é' and 'ê' were stripped, leaving 't'

// Emoji palindrome using spread operator
console.log([...'🎉🎊🎉'].reverse().join(''))
// Result: '🎉🎊🎉' — spread handles surrogate pairs correctly

// split('') would break emoji
console.log('🎉🎊🎉'.split('').reverse().join(''))
// Result: garbled output — split breaks surrogate pairs

The \p{L} Unicode property matches any letter from any language, and \p{N} matches any numeric digit. The u flag enables Unicode mode. Always use the spread operator ([...str]) instead of split('') when the string might contain emoji or characters outside the Basic Multilingual Plane. For more on removing unwanted characters, see how to remove special characters from a string in JavaScript.

4. Checking Numeric Palindromes

Numbers can also be palindromes (121, 1331, 12321). You can check numeric palindromes by converting to a string, or without string conversion for better performance.

// String-based approach
const isNumericPalindrome = (num) => {
  const str = String(num)
  return str === [...str].reverse().join('')
}

console.log(isNumericPalindrome(121))
// Result: true

console.log(isNumericPalindrome(123))
// Result: false

console.log(isNumericPalindrome(-121))
// Result: false (negative sign makes it asymmetric)

// Pure numeric approach (no string conversion)
const isNumPalindrome = (num) => {
  if (num < 0) return false
  if (num < 10) return true

  let reversed = 0
  let original = num

  while (original > 0) {
    reversed = reversed * 10 + (original % 10)
    original = Math.floor(original / 10)
  }

  return reversed === num
}

console.log(isNumPalindrome(12321))
// Result: true

console.log(isNumPalindrome(10))
// Result: false

// Find palindromic numbers in a range
const palindromeNumbers = Array.from({ length: 200 }, (_, i) => i + 1)
  .filter(isNumPalindrome)
// Result: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88,
//          99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]

The pure numeric approach avoids string allocation entirely, making it ideal for batch processing large datasets of numbers. Negative numbers are never palindromes because the minus sign has no counterpart at the end.

5. Finding Palindromic Substrings

Beyond checking if an entire string is a palindrome, a common challenge is finding all palindromic substrings or the longest palindrome within a string.

// Find the longest palindromic substring
const longestPalindrome = (text) => {
  if (text.length < 2) return text
  let start = 0
  let maxLen = 1

  const expandFromCenter = (left, right) => {
    while (left >= 0 && right < text.length && text[left] === text[right]) {
      const len = right - left + 1
      if (len > maxLen) {
        start = left
        maxLen = len
      }
      left--
      right++
    }
  }

  for (let i = 0; i < text.length; i++) {
    expandFromCenter(i, i)     // odd-length palindromes
    expandFromCenter(i, i + 1) // even-length palindromes
  }

  return text.slice(start, start + maxLen)
}

console.log(longestPalindrome('babad'))
// Result: 'bab' (or 'aba')

console.log(longestPalindrome('cbbd'))
// Result: 'bb'

console.log(longestPalindrome('racecarxyz'))
// Result: 'racecar'

// Count all palindromic substrings
const countPalindromes = (text) => {
  let count = 0

  const expand = (left, right) => {
    while (left >= 0 && right < text.length && text[left] === text[right]) {
      count++
      left--
      right++
    }
  }

  for (let i = 0; i < text.length; i++) {
    expand(i, i)
    expand(i, i + 1)
  }

  return count
}

console.log(countPalindromes('abc'))
// Result: 3 (a, b, c)

console.log(countPalindromes('aaa'))
// Result: 6 (a, a, a, aa, aa, aaa)

The “expand from center” technique runs in O(n²) time and O(1) space — much better than checking every possible substring. It works by treating each character (and each gap between characters) as a potential palindrome center, then expanding outward while characters match.

6. Palindrome Validation in Forms

Palindrome checking can be integrated into form validation — for example, detecting if a user accidentally entered a symmetric pattern that might indicate placeholder or test data.

// Detect suspicious palindromic input in form fields
const isSuspiciousInput = (value) => {
  const cleaned = value.toLowerCase().replace(/[^a-z0-9]/g, '')
  if (cleaned.length < 4) return false // too short to be suspicious

  const reversed = [...cleaned].reverse().join('')
  if (cleaned === reversed) {
    return { suspicious: true, reason: 'Input is a palindrome' }
  }

  // Check if mostly palindromic (first/last halves are very similar)
  const half = Math.floor(cleaned.length / 2)
  const firstHalf = cleaned.slice(0, half)
  const secondHalf = [...cleaned.slice(-half)].reverse().join('')
  const matchCount = [...firstHalf].filter((c, i) => c === secondHalf[i]).length
  const similarity = matchCount / half

  if (similarity > 0.8) {
    return { suspicious: true, reason: 'Input is mostly palindromic' }
  }

  return { suspicious: false }
}

console.log(isSuspiciousInput('abcba'))
// { suspicious: true, reason: 'Input is a palindrome' }

console.log(isSuspiciousInput('abcde'))
// { suspicious: false }

console.log(isSuspiciousInput('abcdcba'))
// { suspicious: true, reason: 'Input is a palindrome' }

// Integration with form validation
const validateField = (value) => {
  if (!value.trim()) return 'Field is required'

  const check = isSuspiciousInput(value)
  if (check.suspicious) return `Warning: ${check.reason}`

  return null // valid
}

This pattern is useful in CoreUI form validation to flag potentially invalid test data before submission. For more on handling empty input, see how to check if a string is empty in JavaScript.

Best Practice Note:

For most use cases, the basic reversal approach is sufficient and the most readable. Use the two-pointer method when processing very long strings or in performance-critical loops. Always normalize input (lowercase + strip non-alphanumeric) before checking — this is the standard definition used in algorithm challenges and CoreUI form controls. Use the spread operator ([...str]) instead of split('') to correctly handle Unicode characters and emoji. For the reverse operation itself, see how to reverse a string in JavaScript.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author