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.



