# Floki: CSS selectors over a three part tuple tree

> Floki gives Elixir a CSS selector layer over parsed HTML and hands back every node as a {tag_name, attributes, children_nodes} tuple. Its default backend is a patched Erlang parser that is documented as slower and as diverging from the HTML5 spec on title text, which puts backend selection in the first hour rather than the last.

**philss/floki** — Floki is a simple HTML parser that enables search for nodes using CSS selectors.

- Repository: https://github.com/philss/floki
- Website: https://hex.pm/packages/floki
- Stars: 2,151 · Forks: 165
- Language: Elixir
- License: MIT
- Published: 2026-10-13 · Updated: 2026-10-13 · Language: en
- Canonical page: https://hysenlabs.com/projects/philss-floki

## CSS selector queries for markup with no DOM in Elixir

Elixir ships no DOM, so code that receives a page from a crawler, a scrape job or a test fixture is left holding a string. Floki turns that string into a nested tuple tree through `Floki.parse_document`, and `Floki.find` walks that tree with a CSS selector and hands back the matching nodes. Selectors are the entry point rather than something bolted on later. The audience is backend teams that need one element out of a page, and test suites that want a real parse instead of a regular expression which quietly stops matching after a markup change. Nothing here renders, executes or fetches anything: the input is a string you already hold and the output is data you can pattern match.

## Adding Floki to mix.exs and running a first selector

Floki is an ordinary Mix dependency. Add it to the deps list in `mix.exs`, then fetch it with `mix deps.get`.

```elixir
defp deps do
  [
    {:floki, "~> 0.38.0"}
  ]
end
```

The requirement `~> 0.38.0` reads like a fixed version but is not one. It admits any 0.38.x build, so three newer patch tags already sit inside the constraint: v0.38.2 from 2026-05-19, v0.38.3 from 2026-05-21 and v0.38.4 from 2026-06-16. None of them needs an edit to `mix.exs`.

For Livebook and one-off scripts the same package goes in through `Mix.install/2`:

```elixir
Mix.install([
  {:floki, "~> 0.38.0"}
])
```

Compiling needs the `:leex` module, which normally arrives with a complete Erlang install. When the build stops on the message module :leex is not available, the documented fix is to install `erlang-dev` and `erlang-parsetools`, and those package names differ between operating systems. Issue 35 collects the error. On the sample page used in the docs, one query returns a single node and piping it back out rebuilds the original tag.

## Every node is a three part tuple, including bare text

A parsed document is a nested list of three element tuples, `{tag_name, attributes, children_nodes}`, where attributes are themselves two element tuples and the children slot mixes text strings with further nodes.

```elixir
document
|> Floki.find(".example")
|> Floki.attribute("class")
# => ["example"]

document
|> Floki.find(".headline")
|> Floki.text()
# => "Floki"
```

`Floki.attribute` runs the same lookup from a document plus a selector and returns a flat list of strings. Two consequences matter when you write pattern matches. Text is always wrapped in a list, so `["Floki"]` and `[{"b", [], [" bold "]}, " text "]` are both children lists, and code assuming a bare string in that slot will miss. Parsing a full document keeps the `html` and `body` wrappers in the tree, which is why a node matched by `.example` comes back nested inside two tuples rather than at the top level.

## The default parser splits title content the spec keeps as text

The default backend is a patched version of `mochiweb_html`, kept as the default because it is written in Erlang and needs no outside dependencies. Correctness is where it parts company with a parser that follows the specification. Content inside a title element is plaintext under HTML5, so a correct parser produces `{"title", [], [" <b> bold </b> text "]}` while the patched Erlang parser returns `{"title", [], [{"b", [], [" bold "]}, " text "]}`, inventing a `b` child node the specification says should not exist. Any selector reaching into a title, and any code that counts children or reads text from them, gets a different answer depending on which backend is compiled in. The README does not document which results change per backend, so this is a case to test rather than look up.

## The 20 times figure comes from fast_html's own benchmark page

The speed number attached to the default parser is not a Floki measurement. The documentation links the claim that the default parser can be up to 20 times slower than the alternatives on big HTML documents to the fast_html benchmark page, and the same section reports that fast_html is faster according to benchmarks conducted by its developers. Read that as a vendor comparison rather than a neutral result. Floki does ship a harness if you want numbers from your own pages:

```sh
sh benchs/extract.sh
mix run benchs/parse_document.exs
```

`benchs/extract.sh` prepares the fixture files and is needed only once, then `mix run benchs/parse_document.exs` runs the parse benchmark. The repository layout supports that work: a `benchs/` directory sits at the top level next to `lib/`, and `.credo.exs` plus `.formatter.exs` cover static analysis and formatting.

## Swapping backends through the html_parser config key

Changing parser is one dependency plus one config line. `html5ever` wraps the Rust html5ever parser built for the Servo project, reached through a NIF written with `Rustler`, and thanks to `RustlerPrecompiled` nothing has to be installed locally to compile it.

```elixir
defp deps do
  [
    {:floki, "~> 0.38.0"},
    {:html5ever, "~> 0.16.0"}
  ]
end
```

Run `mix deps.get` and `mix compile` to confirm it builds, then select the backend in `config/config.exs`:

```elixir
# in config/config.exs

config :floki, :html_parser, Floki.HTMLParser.Html5ever
```

The same parser can also be passed as an option to `parse_document/2` and `parse_fragment/2`, which is the route when one codebase wants the default parser for most documents and something else for a single page. The last push on this repository was on 2026-09-23, and the newest published tag is v0.38.4 from 2026-06-16.

## The pure C backend is the one that needs a toolchain

`fast_html` wraps lexbor, described as a pure C HTML parser, and it carries a build requirement the other two do not: a C compiler, GNU Make and CMake have to be present on the system so lexbor can be compiled.

```elixir
defp deps do
  [
    {:floki, "~> 0.38.0"},
    {:fast_html, "~> 2.0"}
  ]
end
```

`mix deps.get` and `mix compile` build it, after which the backend is selected the same way as html5ever:

```elixir
# in config/config.exs

config :floki, :html_parser, Floki.HTMLParser.FastHtml
```

That inversion is what to plan around. The Rust NIF needs no local toolchain because precompiled binaries are shipped, while the C parser, the one described as pure C, needs a working build environment on every machine and every container image that compiles dependencies. A minimal build image is where this breaks, not the developer's laptop.

## Round trips drop spacing and backends change results

`Floki.raw_html` regenerates markup from the tuple tree and the documentation states that spaces are ignored, so parsing and re-serializing is not a byte exact copy of the input string. Use it to produce a fragment you intend to render, not to diff two documents. The second limit is behavioural: selector results depend on the backend, as the title case shows, so a test suite asserting exact node shapes is asserting the parser too. Changing `html_parser` later is a code change with test consequences, and no rollback procedure for a backend you regret is documented. Floki is MIT licensed, so redistribution carries the usual obligation to keep the copyright notice, and CODE_OF_CONDUCT.md alongside CONTRIBUTING.md sit at the top level for outside patches. Version bumps stay inside the 0.38 line for now.

## Conclusion

Floki earns its place in any Elixir service that has to pull specific nodes out of markup it did not author, and the selector layer is the reason to pick it over regex work. Leave the default parser in place for small, well formed fixtures. Before committing to a crawler or a scrape job, run your real pages through html5ever and fast_html as well, because a selector can match different nodes once the backend changes. The ~> 0.38.0 requirement already covers v0.38.4, so no dependency edit is needed on the current patch line. Check the title case first.

## FAQ

### How do I install Floki in an Elixir project?

Add `{:floki, "~> 0.38.0"}` to the deps list in `mix.exs` and run `mix deps.get`. In Livebook or a standalone script, `Mix.install/2` accepts the same tuple.

### What does Floki need to compile, and why does the build stop on the :leex error?

Floki needs the `:leex` module, which normally ships with a complete Erlang installation. When the build reports that the module is not available, the fix is to install the `erlang-dev` and `erlang-parsetools` packages, whose names vary by operating system.

### How do I switch Floki to the html5ever or fast_html parser?

Add the backend to your deps, then set `config :floki, :html_parser, Floki.HTMLParser.Html5ever` or `Floki.HTMLParser.FastHtml` in `config/config.exs`. The parser can also be passed per call to `parse_document/2` and `parse_fragment/2`.

### Why does the default Floki parser split title text that the HTML5 spec treats as plaintext?

The default backend is a patched mochiweb_html, and the documentation shows it returning `{"title", [], [{"b", [], [" bold "]}, " text "]}` where a spec compliant parser keeps the content as one string. html5ever and fast_html are the two supported alternatives to compare against.

### Does Floki round trip HTML byte for byte, and how do I benchmark the parsers?

`Floki.raw_html` regenerates markup and ignores spaces, so a parse and serialize cycle is not an exact copy of the input. For your own numbers the repository ships `benchs/extract.sh`, needed once, and `mix run benchs/parse_document.exs` to run the parse benchmark.

## Sources

- [License: MIT](https://github.com/philss/floki/blob/main/LICENSE)
- [philss/floki on GitHub](https://github.com/philss/floki)
- [Project website](https://hex.pm/packages/floki)
- [README](https://github.com/philss/floki/blob/main/README.md)
- [Releases](https://github.com/philss/floki/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/philss-floki
