Plain is headed towards 1.0! Subscribe for development updates →

plain.tailwind

Integrate Tailwind CSS without JavaScript or npm.

Made possible by the Tailwind standalone CLI, which is installed for you.

$ plain tailwind
Usage: plain tailwind [OPTIONS] COMMAND [ARGS]...

  Tailwind CSS

Options:
  --help  Show this message and exit.

Commands:
  compile  Compile a Tailwind CSS file
  init     Install Tailwind, create a tailwind.config.js...
  update   Update the Tailwind CSS version

Installation

Add plain.tailwind to your INSTALLED_PACKAGES:

# settings.py
INSTALLED_PACKAGES = [
    # ...
    "plain.tailwind",
]

Create a new tailwind.config.js file in your project root:

plain tailwind init

This will also create a tailwind.css file at static/src/tailwind.css where additional CSS can be added. You can customize where these files are located if you need to, but this is the default (requires STATICFILES_DIR = BASE_DIR / "static").

The src/tailwind.css file is then compiled into dist/tailwind.css by running tailwind compile:

plain tailwind compile

When you're working locally, add --watch to automatically compile as changes are made:

plain tailwind compile --watch

Then include the compiled CSS in your base template <head>:

{% tailwind_css %}

In your repo you will notice a new .plain directory that contains tailwind (the standalone CLI binary) and tailwind.version (to track the version currently installed). You should add .plain to your .gitignore file.

Updating Tailwind

This package manages the Tailwind versioning by comparing the value in your pyproject.toml to .plain/tailwind.version.

# pyproject.toml
[tool.plain.tailwind]
version = "3.4.1"

When you run tailwind compile, it will automatically check whether your local installation needs to be updated and will update it if necessary.

You can use the update command to update your project to the latest version of Tailwind:

plain tailwind update

Adding custom CSS

If you need to actually write some CSS, it should be done in app/static/src/tailwind.css.

@tailwind base;


@tailwind components;

/* Add your own "components" here */
.btn {
    @apply bg-blue-500 hover:bg-blue-700 text-white;
}

@tailwind utilities;

/* Add your own "utilities" here */
.bg-pattern-stars {
    background-image: url("/static/images/stars.png");
}

Read the Tailwind docs for more about using custom styles →

Deployment

If possible, you should add static/dist/tailwind.css to your .gitignore and run the plain tailwind compile --minify command as a part of your deployment pipeline.

When you run plain tailwind compile, it will automatically check whether the Tailwind standalone CLI has been installed, and install it if it isn't.

When using Plain on Heroku, we do this for you automatically in our Plain buildpack.

  1import os
  2
  3import click
  4
  5from plain.packages import packages
  6from plain.runtime import APP_PATH
  7
  8from .core import Tailwind
  9
 10
 11@click.group("tailwind")
 12def cli():
 13    """Tailwind CSS"""
 14    pass
 15
 16
 17@cli.command()
 18def init():
 19    """Install Tailwind, create a tailwind.config.js and app/assets/src/tailwind.css"""
 20    tailwind = Tailwind()
 21
 22    tailwind_installed = tailwind.is_installed()
 23
 24    if not tailwind_installed:
 25        # Need to do an initial download to run init
 26        tailwind.download()
 27
 28    # Config needs to exist first so we can save the version here
 29    if not tailwind.config_exists():
 30        click.secho("Creating Tailwind config...", bold=True)
 31        tailwind.create_config()
 32
 33    if not tailwind_installed:
 34        # Mostly need to save the version to config...
 35        click.secho("Installing Tailwind standalone...", bold=True, nl=False)
 36        version = tailwind.install()
 37        click.secho(f"Tailwind {version} installed", fg="green")
 38
 39    if not tailwind.src_css_exists():
 40        click.secho("Creating Tailwind source CSS...", bold=True)
 41        tailwind.create_src_css()
 42
 43
 44@cli.command()
 45@click.option("--watch", is_flag=True)
 46@click.option("--minify", is_flag=True)
 47def compile(watch, minify):
 48    """Compile a Tailwind CSS file"""
 49    tailwind = Tailwind()
 50
 51    if not tailwind.is_installed() or tailwind.needs_update():
 52        version_to_install = tailwind.get_version_from_config()
 53        if version_to_install:
 54            click.secho(
 55                f"Installing Tailwind standalone {version_to_install}...",
 56                bold=True,
 57                nl=False,
 58            )
 59            version = tailwind.install(version_to_install)
 60        else:
 61            click.secho("Installing Tailwind standalone...", bold=True, nl=False)
 62            version = tailwind.install()
 63        click.secho(f"Tailwind {version} installed", fg="green")
 64
 65    args = []
 66    args.append("-i")
 67    print(f"Input: {tailwind.src_css_path}")
 68    args.append(tailwind.src_css_path)
 69
 70    args.append("-o")
 71    print(f"Output: {tailwind.dist_css_path}")
 72    args.append(tailwind.dist_css_path)
 73
 74    # These paths should actually work on Windows too
 75    # https://github.com/mrmlnc/fast-glob#how-to-write-patterns-on-windows
 76    args.append("--content")
 77    paths = [
 78        os.path.relpath(APP_PATH) + "/**/*.{html,js}",
 79    ]
 80
 81    # Add paths from installed packages
 82    for package_config in packages.get_package_configs():
 83        paths.append(os.path.relpath(package_config.path) + "/**/*.{html,js}")
 84
 85    content = ",".join(paths)
 86    print(f"Content: {content}")
 87    args.append(content)
 88
 89    if watch:
 90        args.append("--watch")
 91
 92    if minify:
 93        args.append("--minify")
 94
 95    tailwind.invoke(*args, cwd=os.path.dirname(APP_PATH))
 96
 97
 98@cli.command()
 99def update():
100    """Update the Tailwind CSS version"""
101    tailwind = Tailwind()
102    click.secho("Installing Tailwind standalone...", bold=True, nl=True)
103    version = tailwind.install()
104    click.secho(f"Tailwind {version} installed", fg="green")
105
106
107if __name__ == "__main__":
108    cli()