All posts

Common regex patterns for email, URL and dates

Tested regex patterns for email, URLs, ISO dates, times, IPv4, UUIDs, hex colours and slugs, with what each accepts and rejects and when to use a parser.

By Last reviewed 6 min readRegexValidationJavaScript

Every pattern on this page was run in a JavaScript engine against the accept and reject examples listed under it. They're practical patterns, written to catch typos and obvious junk, not to implement an RFC. Where a regex is the wrong tool for the last step, the caveat says what to use instead.

To check one against your own data, paste the pattern into the regex tester, put your samples one per line and turn on the g and m flags. With m, ^ and $ match at every line break, so each valid line highlights and each invalid one stays plain.

Validate or extract: anchor accordingly

The patterns below are anchored with ^ and $ because they validate a whole value. Without anchors, \d{4}-\d{2}-\d{2} happily accepts x2026-10-25x. When you're pulling values out of a larger text, drop the anchors and guard the edges with \b or lookarounds instead, as in the IPv4 section. Most entries in the tester's Common patterns menu are written that way, unanchored and with g on, so add ^ and $ before you use one for validation.

Watch alternation when you anchor. ^cat|dog$ means "starts with cat, or ends with dog", so it matches hotdog. Wrap the alternatives: ^(?:cat|dog)$.

Email

^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$
  • Accepts: ada@example.com, support+billing@devprix.io, ops@mail.example.co.uk
  • Rejects: bob@, @example.com, ada@example, ada@@example.com, ada@example..com
  • Also accepts, though invalid: .ada@example.com, ada..lovelace@example.com
  • Also rejects, though valid: o'brien@example.ie, "ada lovelace"@example.com, josé@example.com

Why not a "perfect" email regex? The grammar in RFC 5322 allows quoted local parts with spaces, comments in parentheses and IP-address domains like user@[192.168.0.1], and internationalized email adds non-ASCII characters on top. A pattern that covers all of it is unreadable, and it still can't tell you whether the mailbox exists. Even the HTML standard doesn't try: the format it defines for <input type="email"> is described as a willful violation of RFC 5322, and it accepts user@localhost.

The practical approach is a loose pattern like the one above to catch typos while the user is typing, then a confirmation link sent to the address. That's the only check that proves it works. If apostrophes matter to your users, add ' to the first character class.

URL

^https?://(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?::\d{1,5})?(?:[/?#]\S*)?$
  • Accepts: https://devprix.io, https://devprix.io/tools/regex-tester?x=1#top, http://api.example.com:8080/v1/orders
  • Rejects: example.com, ftp://example.com, https://exa mple.com, https://example
  • Also rejects, though valid: https://localhost:3000, http://192.168.0.1/admin, http://[::1]:8080/
  • Also accepts, though invalid: https://example.com:99999

This answers "does it look like a web link with a domain name", nothing more. It doesn't know about IPv6 hosts or internationalized domain names, and it doesn't check that the port is in range. In a JavaScript regex literal, escape each / as \/.

If you need the host, path or query string, don't take the URL apart with more regex. new URL(input) parses by the WHATWG URL rules that browsers follow, and the URL parser shows you the protocol, host, port, path, query parameters and fragment it produces.

For finding links in prose, trailing punctuation is the usual problem: https?://\S+ on see https://devprix.io/tools. includes the full stop. Ending on a character that can't be punctuation fixes the common cases:

https?://[^\s<>"')]*[^\s<>"').,;:!?]

"Docs: https://devprix.io/tools/regex-tester. Also (https://example.com/a), ok"
  → https://devprix.io/tools/regex-tester
  → https://example.com/a

The price is that a URL ending in ), as some Wikipedia links do, loses its last character.

ISO date (YYYY-MM-DD)

^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$
  • Accepts: 2026-10-25, 2024-02-29, 1999-12-31
  • Rejects: 2026-13-01, 2026-00-10, 2026-10-32, 2026-1-5, 2026/10/25, 2026-10-25T10:00:00Z
  • Also accepts, though invalid: 2026-02-31, 2023-02-29

A regex can check the shape and the month and day ranges. It can't know February 2023 had 28 days without becoming an unreadable leap-year calculation, so finish in code. Don't lean on new Date(s) for that step: in V8 (Chrome and Node), new Date("2026-02-31") doesn't fail, it rolls over to 3 March. Check the day against the month's real length instead:

function isRealDate(s) {
  const m = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/.exec(s);
  if (!m) return false;
  const [y, mo, d] = m.slice(1).map(Number);
  const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
  const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  return d <= days[mo - 1];
}

isRealDate("2024-02-29")  // true
isRealDate("2023-02-29")  // false
isRealDate("2026-04-31")  // false

If the value is a full timestamp rather than a date, see when to use ISO 8601 strings versus Unix timestamps.

24-hour time

^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$
  • Accepts: 00:00, 09:30, 23:59:59
  • Rejects: 24:00, 9:30, 12:60, 12:30:5, 12:30 PM, 23:59:60

Hours need two digits; change the start to (?:[01]?\d|2[0-3]) if people type 9:30. RFC 3339 allows a 60th second for leap seconds, but few forms or APIs you'll validate expect one.

IPv4 address

^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$
  • Accepts: 192.168.1.1, 10.0.0.255, 0.0.0.0, 255.255.255.255
  • Rejects: 256.1.1.1, 999.999.999.999, 192.168.1, 1.2.3.4.5, 192.168.01.1

The short version in most cheat sheets, ^(?:\d{1,3}\.){3}\d{1,3}$, checks the shape only and accepts 999.999.999.999. The long one spells out each octet: 250–255, 200–249, 100–199, then 0–99 without a leading zero. Leading zeros are rejected on purpose, because some parsers read them as octal; the browser's URL parser turns http://010.0.0.1 into 8.0.0.1.

Alternative order matters once the anchors are gone. Put [1-9]?\d first and an unanchored octet pattern matches only 25 of 250. To extract addresses from text, guard both ends; plain \b would still pull 1.2.3.4 out of 1.2.3.4.5:

(?<![\d.])(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}(?!\.?\d)

"from 10.0.0.12 and 1.2.3.4.5, end 8.8.8.8."  →  10.0.0.12, 8.8.8.8

UUID

^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$

Use it with the i flag so uppercase hex passes.

  • Accepts: 550E8400-E29B-41D4-A716-446655440000, 019a1b2c-3d4e-7f60-8a9b-0c1d2e3f4a5b, 00000000-0000-0000-0000-000000000000
  • Rejects: 550e8400e29b41d4a716446655440000 (no hyphens), {550e8400-e29b-41d4-a716-446655440000} (braces), 550e8400-e29b-41d4-a716-44665544000 (one digit short)

That checks the 8-4-4-4-12 layout only. To check the version and variant fields from RFC 9562 as well, require 1–8 at the start of the third block and 8, 9, a or b at the start of the fourth (again with i):

^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
  • Accepts: 550e8400-e29b-41d4-a716-446655440000 (version 4), 019a1b2c-3d4e-7f60-8a9b-0c1d2e3f4a5b (version 7)
  • Rejects: 00000000-0000-0000-0000-000000000000 (the Nil UUID), 550e8400-e29b-41d4-c716-446655440000 (wrong variant)

The Nil and Max UUIDs fail the strict version, so allow them separately if your system uses them. For test data, generate a batch of v4 and v7 UUIDs and paste them in next to a few broken ones.

Hex colour

^#(?:[0-9a-fA-F]{3}){1,2}$
  • Accepts: #fff, #FF5733, #1a2b3c
  • Rejects: fff, #ggg, #12345, #ffff, #ff57331a

Three or six digits is the classic form. CSS also accepts four and eight, where the last one or two digits set the alpha channel. To allow those:

^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$
  • Accepts: #fff8, #ff57331a
  • Rejects: #12345, #1234567

URL slug

^[a-z0-9]+(?:-[a-z0-9]+)*$
  • Accepts: regex-tester, hs256-vs-rs256, 2026-recap
  • Rejects: Regex-Tester, -leading, trailing-, double--hyphen, under_score, café

Runs of lowercase letters and digits joined by single hyphens. Every hyphen has to be followed by at least one letter or digit, so leading, trailing and doubled hyphens are rejected without any lookarounds.

Next steps

Load any of these into the regex tester with your real data, one value per line with g and m on. The regex cheat sheet has simpler versions of several in its Common Patterns section. If a pattern will also run in Python or Java, read how regex differs between JavaScript, Python and Java first: in both, $ also matches just before a trailing newline.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.