URL Encoding Explained: Why Spaces Become %20 or +
URL encoding, also called percent-encoding, replaces a character in a URL with a percent sign followed by two hexadecimal digits. You need it for characters that aren't allowed in a URL and for characters that already have a special meaning there. A space becomes %20 because a space is byte 32 in ASCII, and 32 in hexadecimal is 20. Every other character, including accented letters and emoji, follows the same rule. You take the character's UTF-8 bytes and write each byte as % plus two hex digits.
The core rules come from RFC 3986, the standard that defines URI syntax. HTML forms use a separate, older format, which is why you sometimes see + instead of %20. Two of the most common encoding bugs are mixing up these two formats and encoding the same text twice.
This guide covers which characters need encoding, how to work out a percent code by hand, when a plus sign means a space, and which JavaScript function to use.
What URL encoding actually does
A URL is text with a structure: scheme, host, path, query string, and fragment. Characters like /, ?, &, = and # separate those parts. If your data contains one of those characters, a parser can't tell whether it's data or a separator.
Percent-encoding solves this by turning the character into a sequence the parser treats as plain data. Say you want to link to search results for rock & roll:
- Unencoded:
https://example.com/search?q=rock & roll. Spaces aren't allowed in URLs, and the&starts a new parameter. The server seesq=rockplus a stray parameter namedroll. - Encoded:
https://example.com/search?q=rock%20%26%20roll. The server decodesqback to exactlyrock & roll.
Anyone can reverse the encoding, and it hides nothing. Like Base64, it is a way to carry data safely, not a security feature.
Reserved vs unreserved characters (RFC 3986)
RFC 3986 sorts characters into three groups. A character's group tells you whether to encode it.
Unreserved characters never need encoding
These 66 characters are always safe as they are: the letters A–Z and a–z, the digits 0–9, and four symbols: hyphen -, period ., underscore _ and tilde ~. That's 26 + 26 + 10 + 4 = 66. Encoding them is legal but pointless. The standard treats %41 and A as equivalent and says URI normalizers should decode them.
Reserved characters do structural work
RFC 3986 lists two sets of reserved characters:
- General delimiters:
:/?#[]@ - Sub-delimiters:
!$&'()*+,;=
What to do depends on the character's job. When it acts as a separator, leave it alone. When it's part of your data, encode it. The / between path segments stays /. A slash inside a band name like AC/DC becomes %2F inside a single path segment. In a query value a slash is technically allowed, but encoding it (as encodeURIComponent does) is the safe default.
Everything else must be encoded
This group covers the space, the double quote, <, >, backslash, ^, backtick, {, }, the vertical bar, control characters such as line breaks, and every non-ASCII character. A literal percent sign must also be encoded, as %25. Otherwise a decoder would read it as the start of an escape sequence.
How a character becomes a percent code: the UTF-8 step
You can percent-encode any character in four steps:
- Find the character's Unicode code point.
- Convert the code point to UTF-8 bytes.
- Write each byte as two hexadecimal digits.
- Put
%in front of each byte.
For basic ASCII characters (code points 0–127), UTF-8 uses one byte equal to the code point. A space is decimal 32, which is 2 × 16 + 0, so hex 20, giving %20. An ampersand is decimal 38 = 2 × 16 + 6, so hex 26, giving %26.
Worked example: é
- Code point:
éis U+00E9, which is decimal 233. - UTF-8 bytes: 233 is above 127, so UTF-8 needs two bytes. Hex
E9in binary is1110 1001. Padded to 11 bits, that's00011 101001. UTF-8's two-byte pattern is110xxxxx 10xxxxxx. Filling in the bits gives11000011 10101001. - Hex:
11000011=C3and10101001=A9. - Result:
%C3%A9.
So café in a URL becomes caf%C3%A9. To check the binary-to-hex steps, the number base converter shows binary, decimal and hex side by side.
Larger code points need more bytes. The euro sign € (U+20AC) takes three bytes, E2 82 AC, so it becomes %E2%82%AC. The grinning face emoji 😀 (U+1F600) takes four bytes, F0 9F 98 80, so it becomes %F0%9F%98%80. That's 12 characters in the URL for one visible symbol.
Why UTF-8 and not something else
Modern browsers and JavaScript's encoding functions use UTF-8, and RFC 3986 recommends UTF-8 for text in new URI schemes. Some older systems used single-byte encodings like Latin-1, where é was just %E9. Suppose you decode a URL and get a garbled character or a decoding error where you expected é. A legacy encoding is a likely cause. JavaScript's old escape() function produces these non-UTF-8 results and is deprecated, so avoid it.
Common URL encodings table
| Character | Name | Encoded | Notes |
|---|---|---|---|
| (space) | Space | %20 |
+ only in form-encoded data |
! |
Exclamation mark | %21 |
Reserved sub-delimiter |
" |
Double quote | %22 |
Not allowed unencoded |
# |
Hash | %23 |
Unencoded, it starts the fragment |
$ |
Dollar sign | %24 |
Reserved sub-delimiter |
% |
Percent | %25 |
Always encode when literal |
& |
Ampersand | %26 |
Separates query parameters |
' |
Apostrophe | %27 |
Reserved sub-delimiter |
( ) |
Parentheses | %28 %29 |
Reserved sub-delimiters |
+ |
Plus | %2B |
Encode in query values to keep it a plus |
, |
Comma | %2C |
Reserved sub-delimiter |
/ |
Slash | %2F |
Separates path segments |
: |
Colon | %3A |
Follows the scheme, separates the port |
; |
Semicolon | %3B |
Reserved sub-delimiter |
= |
Equals | %3D |
Separates key from value |
? |
Question mark | %3F |
Starts the query string |
@ |
At sign | %40 |
Separates user info from host |
[ ] |
Square brackets | %5B %5D |
Reserved for IPv6 hosts |
| (newline) | Line feed | %0A |
Carriage return is %0D |
é |
e with acute | %C3%A9 |
Two UTF-8 bytes |
€ |
Euro sign | %E2%82%AC |
Three UTF-8 bytes |
Hex digits are not case-sensitive, so %2f and %2F decode to the same thing. RFC 3986 recommends uppercase for consistency.
%20 vs + for spaces in form data
Under RFC 3986, a space is always %20. HTML forms, however, submit data in a format called application/x-www-form-urlencoded, which predates RFC 3986. In that format a space becomes +, and a literal plus sign must be written as %2B. If you submit rock & roll through a standard search form, the query string becomes q=rock+%26+roll.
The key point: + means a space only when the text is parsed as form data. That usually happens with query strings and form POST bodies. In a URL path, + is just a plus sign, so /files/a+b.pdf points to a file named a+b.pdf.
That leads to a few practical rules:
- Path segments: use
%20. A+here stays a plus. - Query strings:
%20is always safe.+works only if the server decodes the query as form data. Many web frameworks do, but check yours. - Literal plus signs: always send
%2Bin query values. Otherwise phone numbers like+1 555 0100or email addresses like[email protected]arrive with a space where the plus was.
Each language gives the two styles different names:
| Language | Spaces as %20 |
Spaces as + |
|---|---|---|
| JavaScript | encodeURIComponent() |
URLSearchParams |
| Python | urllib.parse.quote() |
urllib.parse.quote_plus() |
| PHP | rawurlencode() |
urlencode() |
encodeURI vs encodeURIComponent in JavaScript
JavaScript has two built-in encoders. They differ in which reserved characters they leave alone.
encodeURIis meant for a complete URL. It leaves the structural characters; , / ? : @ & = + $ #unencoded, along with unreserved characters and! ' ( ) *.encodeURIComponentis meant for one piece of a URL, such as a single query value or path segment. It encodes every reserved character except! ' ( ) *, so the only characters it leaves unchanged areA–Z a–z 0–9 - _ . ! ~ * ' ( ).
Both use UTF-8 and turn spaces into %20. Here they are side by side:
const value = "AC/DC & more?";
encodeURI(value);
// "AC/DC%20&%20more?"
encodeURIComponent(value);
// "AC%2FDC%20%26%20more%3F"
encodeURI leaves /, & and ? alone, so its result will break the URL if you put it in a query string. encodeURIComponent encodes all three, which is what you want for a value.
Which one to use
Use encodeURIComponent by default for each individual value, then build the URL yourself:
const url = "https://example.com/search?q=" + encodeURIComponent(value);
Use encodeURI only when you already have a complete, correctly structured URL that contains spaces or non-ASCII characters and just needs to be made valid. For query strings, URLSearchParams is often simpler. new URLSearchParams({ q: "rock & roll" }).toString() returns q=rock+%26+roll, which uses the form-data style with +.
encodeURIComponent leaves ! ' ( ) * unencoded, so a few strict systems may still reject its output. The MDN page on encodeURIComponent shows a short wrapper that encodes those characters too.
Decoding pitfalls
decodeURIComponent does not turn + into a space, so decoding rock+%26+roll with it gives rock+&+roll. For form-encoded data, use URLSearchParams or replace + with a space before decoding. decodeURIComponent also throws a URIError on malformed input, such as a lone % or an incomplete sequence like %E2%82. Wrap it in error handling when the input comes from users.
Double encoding and other common bugs
Double encoding
Double encoding happens when already-encoded text gets encoded again. The second pass turns each % into %25:
- Original:
hello world - Encoded once:
hello%20world - Encoded twice:
hello%2520world
The server decodes once and gets hello%20world, so users see a literal %20 in a page title, filename or search box. The giveaway is %25 followed by two hex digits, such as %2520 or %253A.
This often happens when two layers each try to help. For example, you call encodeURIComponent and then pass the result to URLSearchParams or an HTTP library that encodes it again. The fix is to store and pass around raw values, and encode exactly once, where the URL is assembled.
The reverse bug, double decoding, can be a security problem. Suppose an application checks a value after one decode and then decodes it again later. A sequence like %252e%252e%252f can then become ../ after the check has already passed. Decode once, then validate.
Encoding a whole URL as a component
Running encodeURIComponent on a full URL produces https%3A%2F%2Fexample.com..., which no longer works as a link. One exception: when a URL is itself a parameter value, as in ?redirect=https%3A%2F%2Fexample.com%2Fhome, encoding it as a component is exactly right.
Forgetting the percent sign itself
A value like 100% cotton must become 100%25%20cotton. If the % stays unencoded, a decoder will either throw an error or misread it. That's especially likely when the next two characters happen to be valid hex digits.
URL encoding quick reference
- Encode each value separately rather than the assembled URL. (
encodeURIis the exception, for fixing up an already-valid URL.) - In JavaScript, use
encodeURIComponentfor values andURLSearchParamsfor whole query strings. Expect+for spaces fromURLSearchParams. - A space is
%20anywhere.+means a space only in form-encoded query strings and bodies. - A literal
+is%2B, and a literal%is%25. - For non-ASCII text, take the UTF-8 bytes and write each one as
%plus two hex digits:éis%C3%A9,€is%E2%82%AC. - If you see
%25followed by two hex digits, something encoded the text twice. - Use uppercase hex when you generate URLs. Both cases decode the same.
- To inspect a confusing link, paste it into the URL encoder and decoder. It decodes the percent codes and shows the query parameters one by one.
Frequently Asked Questions
Is URL encoding the same as encryption?
No. Percent-encoding is a public, reversible way of writing characters, and anyone can decode it instantly. Never rely on it to hide passwords, tokens or personal data in a URL. Use HTTPS to protect data in transit, and keep sensitive values out of URLs entirely.
How do I decode a URL with %20 and other codes in it?
Replace each percent sign and the two hex digits after it with the byte they represent, then read the bytes as UTF-8 text. In JavaScript, decodeURIComponent does this, and URLSearchParams also turns + into spaces. You can also paste the URL into the URL encoder and decoder to see the readable version.
Why aren't accented domain names percent-encoded?
Domain names follow a different system called internationalized domain names (IDNA). Non-ASCII labels are converted to an ASCII form starting with xn--, called Punycode. For example, münchen.de becomes xn--mnchen-3ya.de. In practice, browsers and DNS use Punycode rather than percent-encoding for host names, and percent-encoding is used in the path, query and fragment.
Do I need to URL-encode letters and numbers?
No. The letters A–Z and a–z, the digits 0–9, and the hyphen, period, underscore and tilde are unreserved, so they never need encoding. Encoding them still works, but the standard treats them as identical to the plain characters, and the encoded versions make URLs harder to read.
What is the difference between URL encoding and Base64?
URL encoding leaves safe characters as they are and escapes only the problem characters, so the text stays mostly readable. Base64 turns any data, including binary files, into a fixed 64-character alphabet and makes it about a third larger. Standard Base64 uses + and /, so it still needs URL encoding, or the URL-safe Base64 variant, before it goes in a URL.