When working with logs or console output in Elixir, you may encounter ANSI escape codes used to style text (e.g., colors or bold). If you're rendering that output in HTML, you'll need to convert those ANSI codes to appropriate CSS classes. Here's a small Elixir helper module that does just that—mapping ANSI codes to TailwindCSS classes.
The problem
ANSI escape codes like \e[31m
(red text) or \e[1m
(bold) are common in CLI output but aren't usable directly in a browser. Instead, we want to transform:
"This is \e[31mred\e[0m and \e[1mbold\e[0m."
...into valid HTML like:
This is <span class="text-red-400">red</span> and <span class="font-bold">bold</span>.
The solution
Here's a lightweight Elixir module that parses ANSI codes and converts them to TailwindCSS utility classes:
defmodule LogColorsHelper do
@ansi_to_tailwind %{
"1" => "font-bold",
"2" => "text-gray-500",
"31" => "text-red-400",
"32" => "text-green-400",
"33" => "text-yellow-300",
"34" => "text-blue-400",
"35" => "text-purple-400",
"36" => "text-cyan-400",
"90" => "text-gray-500",
"97" => "text-gray-100"
}
def ansi_to_tailwind(log) do
regex = ~r/\e\[(\d+(?:;\d+)*)m/
do_ansi_to_tailwind(log, regex, "", false)
end
defp do_ansi_to_tailwind("", _regex, acc, span_open?) do
if span_open?, do: acc <> "</span>", else: acc
end
defp do_ansi_to_tailwind(log, regex, acc, span_open?) do
case Regex.run(regex, log, return: :index) do
nil ->
acc <> (if span_open?, do: log <> "</span>", else: log)
[{match_start, match_len} | _] ->
{before, rest} = String.split_at(log, match_start)
{match, rest_after} = String.split_at(rest, match_len)
codes =
match
|> String.replace(~r/\e\[|\m/, "")
|> String.split(";")
is_reset = Enum.member?(codes, "0")
classes =
unless is_reset do
codes
|> Enum.map(&Map.get(@ansi_to_tailwind, &1))
|> Enum.reject(&is_nil/1)
else
[]
end
span_tag =
if is_reset or classes == [] do
""
else
~s(<span class="#{Enum.join(classes, " ")}">)
end
new_acc =
acc <>
before <>
(if span_open?, do: "</span>", else: "") <>
span_tag
do_ansi_to_tailwind(rest_after, regex, new_acc, not is_reset and classes != [])
end
end
end
The core logic
The work is done recursively in do_ansi_to_tailwind/4
.
defp do_ansi_to_tailwind("", _regex, acc, span_open?) do
if span_open?, do: acc <> "</span>", else: acc
end
When the input string is empty, we close any open span and return the accumulated result.
defp do_ansi_to_tailwind(log, regex, acc, span_open?) do
case Regex.run(regex, log, return: :index) do
nil ->
acc <> (if span_open?, do: log <> "</span>", else: log)
- If no ANSI codes are left, append the rest of the string to the output. If a
<span>
was open, close it.
[{match_start, match_len} | _] ->
{before, rest} = String.split_at(log, match_start)
{match, rest_after} = String.split_at(rest, match_len)
-
Otherwise, we locate the next ANSI sequence using
Regex.run/3
. -
We split the string into:
before
: plain text before the ANSI code,match
: the matched ANSI code,rest_after
: remaining string after the code.
codes =
match
|> String.replace(~r/\e\[|\m/, "")
|> String.split(";")
- We extract the actual SGR codes from the ANSI sequence by removing
\e[
andm
. \e[1;31m
becomes["1", "31"]
.
is_reset = Enum.member?(codes, "0")
- ANSI code
0
means "reset all styles", so we check for it explicitly.
classes =
unless is_reset do
codes
|> Enum.map(&Map.get(@ansi_to_tailwind, &1))
|> Enum.reject(&is_nil/1)
else
[]
end
- If not a reset, we convert the codes to Tailwind classes.
- Unknown codes are ignored (
Map.get/2
will returnnil
).
span_tag =
if is_reset or classes == [] do
""
else
~s(<span class="#{Enum.join(classes, " ")}">)
end
- We generate a new
<span>
tag only if it’s not a reset and there are valid classes.
new_acc =
acc <>
before <>
(if span_open?, do: "</span>", else: "") <>
span_tag
- Append the plain text, close any previously open
<span>
, then insert the new<span>
if needed.
do_ansi_to_tailwind(rest_after, regex, new_acc, not is_reset and classes != [])
- Recurse into the rest of the string.
span_open?
is updated to reflect whether we just opened a new span.
Example
iex> LogColorsHelper.ansi_to_tailwind("This is \e[31mred\e[0m and \e[1mbold\e[0m.")
"This is <span class=\"text-red-400\">red</span> and <span class=\"font-bold\">bold</span>."
Conclusion
This approach makes it easy to render styled logs in a browser using TailwindCSS. You can extend the @ansi_to_tailwind
map to support more ANSI codes or customize the styling for your design system.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.