Sigils Basics
Sigils are Elixir's way of writing literals that the language did not bake into its base syntax. Instead of inventing a new bracket pair every time someone wants a regex, a word list, or a calendar value, the designers gave you one consistent shape — ~x"..." — and let the letter pick the meaning. The result is that Elixir source code is dense with ~r/.../, ~w[...], and ~D[...] rather than the noisy quote-and-escape soup you would write in a language without them.
If you have read any nontrivial Elixir codebase — Phoenix, Ecto, Oban, anything Discord open-sourced — you have already seen sigils everywhere. They are not exotic. They are how you write text-heavy code without losing your mind to backslashes.
The Shape of a Sigil
Every sigil follows the same template:
~<letter><delimiter>content<delimiter><modifiers>
The letter picks the type. The delimiter pair wraps the content. The optional trailing modifiers tweak behavior.
~s"hello world" # a string
~r/^\d+$/i # a case-insensitive regex
~w[apple banana cherry] # a list of words
~D[2026-05-12] # a Date struct
The delimiter can be any of the matched pairs ( ), [ ], { }, < >, or the symmetric pairs " ", ' ', | |, / /. Pick whichever makes the content read cleanly. If your regex contains slashes, write ~r|/foo/bar| instead of escaping. If your word list contains brackets, switch to parens. This is one of those small ergonomic wins that adds up over a codebase.
Lower vs Upper Case
Every sigil has two forms: a lowercase letter and an uppercase letter. The lowercase form interpolates and processes escape sequences. The uppercase form treats the content as raw — no #{...} interpolation, no escape codes.
name = "Alice"
~s"Hello #{name}\n"
# "Hello Alice\n" (with a real newline)
~S"Hello #{name}\n"
# "Hello #{name}\\n" (literal text, no interpolation)
This is mostly useful for embedded code — regex patterns full of backslashes, JSON snippets in test fixtures, anything where you would rather not double-escape. ~S and ~R are the variants you reach for when the content is already meant for another language's parser.
The Sigils You Will Actually Use
There are about ten built-in sigils. In practice, four of them carry almost all the weight.
~r for regular expressions. This is the one you will use most. It compiles a regex at compile time and the result implements the Regex protocol used by String.replace/3, String.split/2, and friends.
email_pattern = ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/
"alice@example.com" =~ email_pattern # true
String.split("a, b,c , d", ~r/\s*,\s*/) # ["a", "b", "c", "d"]
Writing regexes as plain strings — Regex.compile!("^\\d+$") — works but reads like a noise pattern. ~r is one of the things that makes Elixir comfortable for text processing.
~w for word lists. A space-separated list of strings becomes a list literal. With the a modifier, you get atoms instead.
~w(apple banana cherry)
# ["apple", "banana", "cherry"]
~w(read write admin)a
# [:read, :write, :admin]
This is everywhere in Phoenix controller code, Ecto schema definitions, permission lists, and config. The atom variant is the canonical way to write a list of permission names or schema fields.
~s and ~S for strings. Mostly useful when the string contains both single and double quotes, or when you want to avoid escaping. For ordinary strings, plain double quotes are still the right call. Reach for ~s when the content fights you.
~s(He said "don't worry" and walked away)
# "He said \"don't worry\" and walked away"
~c and ~C for charlists. Charlists are single-quoted strings — really lists of Unicode codepoints — and Elixir 1.15 deprecated the single-quote literal syntax in favor of ~c. Most Elixir code does not use charlists, but Erlang interop does, and ~c is now the preferred way to write them.
~c"hello"
# ~c"hello" — a list of integers [104, 101, 108, 108, 111]
Old code with 'hello' still compiles but produces a warning. New code should use ~c.
The remaining built-ins — ~D, ~T, ~N, ~U for dates and times, and the heredoc variants — show up regularly but are less load-bearing. The next subtopic covers them in detail.
Why Sigils Exist
A language could just have more literal syntax. Python has raw strings with r"...", regex with re.compile(...), and dates only via a constructor. Ruby has %w[...], %r{...}, and a half-dozen other percent literals. JavaScript has template literals with backticks, regex with slashes, and no native date literal at all.
Elixir's choice was to pick one shape — ~x"..." — and let it be extensible. You get a consistent reader experience: when you see ~, you know you are looking at a literal of some kind. You do not have to remember which language feature uses which bracket style. And as the next subtopic shows, you can define your own sigils for domain-specific data, which is what makes ~H for Phoenix templates and ~Q for Ecto queries possible.
The alternative — adding more keywords or syntax forms over time — would have left the grammar bloated and the parser fragile. Sigils give you literal-like ergonomics without grammar churn.
Which Ones Are Worth Reaching For
Here is the honest hierarchy, ordered by how often I actually type them:
~r— daily, for any nontrivial text matching.~w— daily, especially~w()afor atom lists in schemas and permissions.~Dand~U— weekly, when working with calendar data without pulling in Timex.~sand~S— occasionally, when escaping gets ugly.~H— constantly if you write Phoenix LiveView, never otherwise.~c— rarely, mostly for Erlang interop.~Nand~T— rarely, unless your domain genuinely needs naive datetimes or wall-clock times.
The ones you should think about before using: custom sigils. They are powerful but easy to overuse. The next-to-last subtopic in this section covers when they pay off.
Interpolation and Escape Sequences
The lowercase forms handle the same escapes you would see in a regular string:
~s"line one\nline two"
# "line one\nline two" — \n is a real newline
~s"name: #{user.name}"
# "name: Alice" — interpolation works
The uppercase forms turn both of these off. Whatever you put between the delimiters is what you get:
~S"line one\nline two"
# "line one\\nline two" — backslash-n stays literal
~S"name: #{user.name}"
# "name: \#{user.name}" — interpolation never runs
This distinction matters most when you are writing content meant for another parser. A JSON snippet in a test fixture should be ~S. A regex pattern with lots of backslashes should be ~r — and remember that ~r already does not interpolate #{} inside character classes by default, so the lowercase form is usually fine.
The set of escapes recognized by the lowercase forms is the same across all sigils: \n, \t, \r, \\, the Unicode escapes \uXXXX, and the hex form \xNN. You do not have to learn a different escape table per sigil.
A Quick Tour Through Real Code
A snippet that would appear in any Phoenix controller:
def index(conn, params) do
allowed_fields = ~w(id name email inserted_at)a
sort_field = Map.get(params, "sort", "id")
if sort_field in Enum.map(allowed_fields, &to_string/1) do
# ...
end
end
A snippet from an Ecto changeset:
def changeset(user, attrs) do
user
|> cast(attrs, ~w(email password role)a)
|> validate_required(~w(email password)a)
|> validate_format(:email, ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
end
A snippet from a test:
test "parses ISO 8601 timestamps" do
assert MyApp.Time.parse("2026-05-12") == ~D[2026-05-12]
assert MyApp.Time.parse_utc("2026-05-12T10:30:00Z") == ~U[2026-05-12 10:30:00Z]
end
In each case, the sigil makes the data look like data instead of like a constructor call. That is the whole pitch.
A Quick Note on Performance
Sigils are mostly a compile-time concern. ~w(a b c) is compiled to a list literal — there is no runtime cost. ~D[2026-05-12] is compiled to a Date struct literal — also free at runtime. ~r/foo/ compiles the regex pattern at compile time when the sigil appears in a position the compiler can see, which is most positions.
The case to watch is regex inside a function body where the pattern depends on a variable. That is rare in practice — you almost never construct regex patterns from runtime data in idiomatic Elixir — but when it happens, you are looking at runtime compilation cost per call. The fix is to assign the regex to a module attribute or compile it once and pass the compiled struct around.
For the calendar sigils, there is no cost difference between ~D[2026-05-12] and Date.new!(2026, 5, 12). Both produce the same struct. The sigil is purely about readability.
If you are profiling and find that a sigil shows up as hot, it almost always means a regex is being recompiled inside a tight loop. The fix is a one-liner — hoist it to a module attribute — and the win is dramatic.
Common Pitfalls
Picking the wrong delimiter. ~r/foo\/bar/ requires escaping the inner slash. ~r|foo/bar| does not. Spending five minutes counting backslashes when a different delimiter would have been clean is a beginner trap that veterans still fall into.
Forgetting that uppercase sigils skip interpolation. Writing ~S"user is #{name}" and wondering why the name does not show up is a five-minute debug the first time. The uppercase form is for raw content. If you want interpolation, use the lowercase form.
Reaching for ~s when "..." would do. Plain string literals already support #{} interpolation and \n escapes. ~s is only useful when you want a non-quote delimiter — for example, to avoid escaping embedded quotes.
Confusing ~w modifiers with regex modifiers. ~w(a b c)a makes atoms; ~r/x/i makes case-insensitive regex. The letters are sigil-specific. There is no master table — you have to learn them per sigil.
Using 'single quotes' for strings. This is the most common newcomer mistake when coming from Ruby or JavaScript. In Elixir, 'hello' is a charlist, not a string, and as of 1.15 it emits a deprecation warning. Use "..." for strings and ~c"..." if you actually want a charlist.
Key Takeaways
- A sigil has the shape
~<letter><delimiter>content<delimiter><modifiers>. - Lowercase sigils interpolate and process escapes; uppercase sigils take content raw.
- Delimiters are interchangeable — pick whichever pair makes the content read cleanly.
- The four you will use constantly:
~rfor regex,~wfor word lists,~s/~Sfor awkward strings, and~cfor charlists in Erlang interop. - Sigils exist so Elixir can add literal-like syntax without growing the grammar — the next subtopic shows the calendar and heredoc sigils, and the one after shows how to define your own.