CLI tool
djlint/djLint avatar
djlint/djLint

djLint: a formatter and linter for HTML templates

HTML template formatter and linter for Django, Jinja, Twig, Nunjucks, Handlebars, Liquid, Go templates, Mustache, Tera, Askama and Angular. Formats markup and template syntax together, and lints for accessibility, correctness and style. CLI, pre-commit hook and VS Code extension.

953 stars126 forksPythonGPL-3.0

At a glance

What is it?
djLint is a Python tool that formats and lints HTML templates across Django, Jinja, Twig, Nunjucks, Handlebars, Liquid, Go templates, Mustache, Tera, Askama and Angular, shipped as a CLI, a pre-commit hook and a VS Code extension. It closes a real gap in template tooling, and its GPL-3.0-or-later licence is the constraint most adopters will need to think about.
Who is it for?
Adopt djLint when your project renders templates through an engine whose syntax a plain HTML formatter cannot parse, and you want one command that fixes indentation, normalises tag case and flags missing alt attributes before review.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

HTML templates are two languages in one file

The problem djLint sets out to solve is stated in the README with unusual clarity. Every language in a stack has a formatter and a linter, and HTML templates are the exception. Generic HTML tools cannot parse the template syntax, and template engines do not care what the markup around them looks like. The result is that indentation drifts, tags go unclosed, spacing goes wherever, and none of it is caught in review.

That diagnosis is precise about why existing tools fail. A template file is not HTML, it is HTML interleaved with a second language, and each engine spells that language differently: Django and Jinja use brace syntax, Twig and Liquid use their own, Go templates use yet another, and Mustache and Handlebars are a third family. A tool that understands one of them cannot parse the other, and a tool that treats the file as HTML will mangle the template tags it does not recognise.

djLint's answer is to parse both, with a profile per engine. The project lists profiles for Django, Jinja, Twig, Nunjucks, Handlebars, Liquid, Go templates, Mustache, Tera, Askama and Angular, and the README states the goal plainly: markup and template syntax formatted together, with linting for accessibility, correctness and style.

The audience is the team that owns the templates rather than the team that owns the design system, and the tool ships in the shapes such a team actually uses it: a command line tool, a pre-commit hook and an editor extension.

One reformat pass fixes indentation, tag case and spacing

The README's worked example is the best available demonstration of what the formatter does, because it shows the before and after of the same template.

The original mixes upper case tags, missing spaces inside the template delimiters, a long anchor tag with several attributes on one line, template blocks with no indentation and stray blank lines. After one command it looks like this:

html
{% block content %}
    <section class="posts">
        <h2>Latest posts</h2>
        {% if posts %}
            <ul>
                {% for post in posts %}
                    <li>
                        <a
                            href="{% url 'post' post.slug %}"
                            class="post-link {% if post.featured %}is-featured{% endif %}"
                            data-analytics-id="post-{{ post.id }}"
                            aria-label="Read {{ post.title }}"
                        >{{ post.title|title }}</a>
                    </li>
                {% endfor %}
            </ul>
        {% else %}
            <p>No posts yet.</p>
        {% endif %}
    </section>
{% endblock %}

The README's own summary of that single command lists what changed: the indentation was rebuilt, the tag case was fixed, the long tag was split into one attribute per line, the template tags were normalised and the stray blank lines were collapsed.

The single attribute per line behaviour is not the default, it is the --single-attribute-per-line flag, which is used consistently throughout the README's own examples. That is a design choice with consequences: it produces verbose but diff friendly output, where a change to one attribute does not touch the whole line.

The formatting of template tags, adding the spaces inside the delimiters and reindenting the blocks, is the part a generic HTML formatter cannot do at all.

Four install paths, and one to avoid

The recommended way to add it to a project is as a development dependency with uv:

bash
uv add --dev djlint

To use it as a standalone tool rather than a project dependency:

bash
uv tool install djlint

With pip:

bash
pip install djlint

With pipx:

bash
pipx install djlint

And there is a community maintained Homebrew formula for macOS and Linux:

bash
brew install djlint

Then there is npm, and the README attaches a warning to it that is worth reading before you copy the command. The npm package is only a wrapper, and its install script runs pip install --upgrade djlint against whatever python3 happens to be on your system path. npm will not manage or uninstall the actual package, so the README recommends preferring pip directly where possible. The package manifest confirms the mechanism, since the postinstall script is python3 -m pip install --upgrade djlint==1.46.6.

That is a real operational hazard rather than a theoretical one. A developer who installs the npm package on a machine with several Python versions gets a tool installed outside the version their project expects, and the package manager they used will not remove it later.

The Python package itself is straightforward. It requires Python 3.11 or newer, the classifiers run up to 3.16, and the runtime dependencies are a short list: click for the command line, cssbeautifier and jsbeautifier for the embedded languages, json5, pathspec, pyyaml and regex. The console script is exposed as djlint.

Four commands cover lint, check, reformat and profile

The command surface is small enough to learn in a minute, and the README presents it as four invocations.

To lint a project:

bash
djlint . --lint

To check whether the formatting is already correct, which is the mode you want in CI:

bash
djlint . --check

To fix the formatting:

bash
djlint . --reformat --single-attribute-per-line

And to select the rules and formatting for your engine:

bash
djlint . --reformat --single-attribute-per-line --profile=django

The profile flag is the one that decides how much of this works correctly on your templates. Without it, djLint has to guess which template language it is looking at, and the formatting rules it applies for an engine are specific to that engine's syntax.

The lint mode covers what formatting cannot fix. The README lists orphan tags, missing alt attributes and hard-coded URLs, and describes the set as dozens of checks, and the project's documentation site has a linter section describing the rules.

For contributors, the README points at the rule definitions themselves, which live in src/djlint/rules.yaml in the repository, so a new check is added by editing that file rather than by patching code. Local setup is two commands, uv sync followed by uv run pytest.

pre-commit and editor support carry the daily workflow

For teams that want formatting enforced before review, the documented route is a pre-commit configuration with two hooks, one that reformats and one that lints:

yaml
repos:
  - repo: https://github.com/djlint/djLint
    rev: v1.46.6 # use the latest version instead
    hooks:
      - id: djlint-reformat
      - id: djlint

The repository carries both .pre-commit-config.yaml and .pre-commit-hooks.yaml, so the hook ids are defined by the project rather than invented by the user. Pinning the rev to a release tag rather than a branch is what makes the hook reproducible.

Editors are covered too. There is a djLint extension for VS Code, listed on the marketplace and on Open VSX, and the integrations documentation covers neovim, Sublime Text and MegaLinter among others.

The ordering of the two hooks matters and is deliberate in the example: the reformat hook runs first so that the lint hook sees already formatted files, which avoids the situation where the formatter and the linter disagree about the same file in the same commit.

One thing to plan for before switching the hooks on is the size of the first commit. A project whose templates have never been formatted will produce a diff across every template file at once, which is unpleasant to review and occasionally conflicts with other work in flight. Running the formatter once on a separate branch is the calmer path.

GPL-3.0-or-later is the constraint to think about first

The licence is GPL-3.0-or-later, declared in the packaging metadata, in the npm manifest and in the repository, and the project ships a LICENSE file together with a LICENSE-THIRD-PARTY file for the dependencies. The README also carries a badge offer for projects that want to advertise the formatting standard.

This is the part of the adoption decision that deserves the most attention, and it is easy to get wrong in both directions.

In one direction, djLint is distributed as an executable tool rather than as a library you import into your product. You run it, it reads and rewrites files, and the output is yours. A formatter invoked as a developer tool over your own source files is a very different situation from linking GPL code into a proprietary application, and most projects in this position treat a command line formatter as an ordinary development dependency.

In the other direction, the actual obligation depends on how you use it and on the licence text itself, which is a legal question rather than a technical one. If you are unsure whether your usage triggers obligations, that is worth checking with whoever handles that in your organisation rather than assuming either way.

Two other facts from the metadata bear on adoption. The package requires Python 3.11 or newer, which is a floor rather than a ceiling given the classifier list. And the project is actively worked on, with the last push to master on 2026-10-19 and version 1.46.6 published on 2026-10-15, following 1.46.5 on 2026-10-14 and 1.46.4 on 2026-10-01. That cadence is fast enough that pinning a version in the pre-commit configuration is not optional.

Two faster alternatives with a different philosophy

The README ends with a section listing other tools of note, and the two it names make a useful contrast because they take a different approach to the same problem.

djade is described as a fast Django template formatter that formats the template syntax while leaving the HTML as it is, and that applies fixes for older Django versions. The difference in approach is the division of labour. djLint formats markup and template syntax together, which gives you one consistent result and a linter on top. djade leaves your HTML untouched, which means your markup keeps whatever indentation and tag case your team already uses, and only the template tags are normalised.

That difference matters most for teams with an existing HTML style convention, or with a large existing codebase where reformatting every template is not something they want to do. The cost is that you end up with two formatting conventions in the same repository unless the HTML is already formatted by another tool.

djangofmt is the second entry, described as a fast HTML-aware Django and Jinja template formatter written in Rust. The point of difference here is implementation language. djLint is Python, with click and the jsbeautifier and cssbeautifier dependencies, and djangofmt is a Rust program.

Neither djade nor djangofmt is a linter in the way djLint is; both concentrate on formatting alone. That is where djLint's own list ends, with its rule set and its accessibility checks.

Editorial conclusion

Adopt djLint when your project renders templates through an engine whose syntax a plain HTML formatter cannot parse, and you want one command that fixes indentation, normalises tag case and flags missing alt attributes before review. Skip it when your codebase cannot take a copyleft dependency, since the package is GPL-3.0-or-later and ships as a separate executable rather than a library you can link, or when a formatter that leaves markup untouched suits you better, since the README points at djade for exactly that. Verify three things first: that the --profile you choose matches the engine your templates use, since the formatting rules are profile specific, that the reformat output on your own templates is acceptable before wiring the pre-commit hook in, because a whole codebase reformat lands as one large diff, and that the npm package is not what you install, since its install script runs pip against whatever python3 is on your PATH. The last push to master was on 2026-10-19 and version 1.46.6 was published on 2026-10-15.

Frequently asked questions

How do I install djLint?

Add it as a development dependency with uv add --dev djlint, or install it standalone with uv tool install djlint, pip install djlint, pipx install djlint or brew install djlint. The README warns against the npm package, whose install script runs pip install --upgrade djlint against whatever python3 is on your system path.

Which template engines does djLint support?

There are profiles for Django, Jinja, Twig, Nunjucks, Handlebars, Liquid, Go templates, Mustache, Tera, Askama and Angular, and you select one with the --profile flag so the right rules and formatting are applied.

What does djLint check when linting?

The linter catches what formatting cannot fix, including orphan tags, missing alt attributes and hard-coded URLs, alongside dozens of other checks. The rule definitions live in src/djlint/rules.yaml in the repository.

Can I use djLint as a pre-commit hook?

Yes. The documented configuration uses two hooks from the repository, djlint-reformat and djlint, pinned to a release tag in the rev field, so formatting runs before linting in the same commit.

What licence is djLint released under?

The package is licensed GPL-3.0-or-later, with the LICENSE file and a LICENSE-THIRD-PARTY file in the repository. It requires Python 3.11 or newer and the console script is named djlint.

Official sources

  1. djlint/djLint on GitHub
  2. License: GPL-3.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/djlint-djlint.svg)](https://hysenlabs.com/projects/djlint-djlint)

Was this useful?