{% extends "base.html" %} {% from "_parts.html" import code, defs, lesson, prose, stage %} {% from "ui/layout.html" import grid, page_header, stack %} {# The landing page: what fjkit is, a first page in four steps, and which page answers which question next. Every claim here is demonstrated on another page — the wiring and htmx on Learn, the signatures on Components — so this page stays short and hands over. The one thing it must not hand over is the first run: a reader who stops after 01 has a working page. House style: one idea per sentence, the takeaway first. #} {% set sections = [ {"id": "start", "num": "01", "title": "Get started"}, {"id": "who", "num": "02", "title": "Who it is for"}, {"id": "box", "num": "03", "title": "What ships in the wheel"}, {"id": "next", "num": "04", "title": "Where to go next"}, ] %} {# Three, and they are the three that decide whether someone keeps reading. The reasoning behind each is in CHARTER.md §2. Plain strings, not Markup: a feature that needs a in it has not been written plainly enough for a landing page. #} {% set features = [ ("No build step", "No package.json, no node_modules, no watcher. The stylesheet is compiled when fjkit is released, not when your app runs."), ("One colour knob", "Every colour is a token. Rebranding is one stylesheet, and status colours stay put, so green still means done."), ("The htmx endpoint is also the JSON API", "A route that renders a partial answers htmx with HTML and every other caller with its response model as JSON."), ] %} {% block content %} {% call stack(8) %} {% set intro %} Pages, tables, forms, navigation and htmx swaps for FastAPI, written as Jinja macros next to your routes. {% endset %} {{ page_header("A Fast UI for FastAPI", intro) }} {# The problem, before the features. What this page used to open with was three features, and a feature only means something to a reader who already has the problem. Two paragraphs, naming the two routes a FastAPI author actually takes today and what each one costs, so "no build step" arrives as the answer to something. Neither route is named as a product. A comparison against named tools is a page that goes stale on somebody else's release schedule. #} {% call prose() %}

A FastAPI service that needs a real interface has two usual routes. Write Jinja and hand-roll the CSS, and you own a design system you did not set out to build. Reach for a front-end framework, and you own a second build, a second language and a second thing to deploy.

fjkit is a third route. The interface is Jinja macros beside your routes, and the stylesheet was compiled when fjkit was released — so your repository gets no package.json, and there is nothing to rebuild when you edit a template.

{% endcall %} {# The proof, before the pitch. A UI kit whose landing page shows nothing it rendered is asking to be taken on trust. Two files, swapped by the theme. The claim in the caption includes dark mode, and one picture can only make half of it. The cost is recorded in `assets/brand.css`, GAP 2. #} {% set shot_caption %}The demo in examples/fjkit-demo. Navigation, dark mode, a filterable table, a form that validates and htmx swaps — and no .css or .js file anywhere in that app.{% endset %} {% call stage("A page the kit rendered", shot_caption, padded=false) %} A task board: a sidebar of nine sections, a new-task form, a table of eight tasks with status and priority badges, and a progress card. The same task board in dark mode. {% endcall %} {{ defs("The key features are:", features) }} {# ============================================================== 01 #} {% set i01 %}Install one package, write a handler and a template, run the app.{% endset %} {% call lesson("start", "01", "Get started", i01) %} {% call prose() %}

Requires Python 3.13 or newer.

{% endcall %} {{ code("uv add fjkit", "bash", label="1 · install") }} {{ code("from pathlib import Path from fastapi import FastAPI from fjkit import FjkitConfig, mount_fjkit, render from pydantic import BaseModel app = FastAPI() mount_fjkit(app, FjkitConfig(template_dir=Path(__file__).parent / \"templates\")) class Overview(BaseModel): done: int @app.get(\"/\") @render(\"overview.html\") def overview() -> Overview: return Overview(done=18)", label="2 · app/main.py") }} {% call prose() %}

mount_fjkit serves the stylesheet and the vendored JavaScript and builds the Jinja environment. @render goes below @app.get and passes the fields of the returned model to the template.

{% endcall %} {{ code("{% extends \"ui/shell.html\" %} {% from \"ui/layout.html\" import page_header, grid %} {% from \"ui/data.html\" import stat %} {% block content %} {{ page_header(\"Overview\", \"How the board is doing\") }} {% call grid(cols=4) %} {{ stat(\"Done\", done, tone=\"success\", icon_name=\"check\") }} {% endcall %} {% endblock %}", "jinja", label="3 · app/templates/overview.html") }} {{ code("uv run fastapi dev app/main.py", "bash", label="4 · run") }} {% call prose() %}

Open http://127.0.0.1:8000. The page has a header, a dark-mode toggle and one stat card. The template has no CSS classes and no colours: macros take named options such as tone="success", and the shipped stylesheet covers them.

Add uv run fjkit check app/templates to CI. It fails when a template uses a class or a colour outside the kit's vocabulary.

{% endcall %} {% endcall %} {# ============================================================== 02 #} {% set i02 %}A back-end developer who needs a real interface and no front-end toolchain.{% endset %} {% call lesson("who", "02", "Who it is for", i02) %} {% call prose() %}

You write FastAPI and need navigation, dark mode, a filterable table and a form that validates. If you want React, Vue or Svelte, use them instead: fjkit renders on the server and has no client-side component model.

{% endcall %} {{ defs("What it costs", [ ("a first page", "about twenty lines, as in 01, with no build step"), ("an admin screen", "a list, filters, a create form and htmx updates, with no CSS and no JavaScript"), ]) }} {% endcall %} {# ============================================================== 03 #} {% set i03 %}One wheel, two runtime dependencies, and nothing for you to compile.{% endset %} {% call lesson("box", "03", "What ships in the wheel", i03) %} {# The three numbers come from `css`, measured by `build.py` against the stylesheet this site links to. They were typed here once and were wrong. #} {% set css_line %}the compiled stylesheet, one per style pack. {{ css.raw_kb }} KB raw, {{ css.gzip_kb }} KB gzip; the build fails above {{ css.budget_kb }} KB. A page loads one.{% endset %} {% call grid(cols=2, gap=4) %} {{ defs("Shipped in the package", [ ("templates/ui/", "the macros: layout, buttons, data, forms, tables, tabs, navigation, sidebar, feedback, icons, and the document shell"), ("static/dist/fjkit-.css", css_line), ("static/vendor/", "htmx and Basecoat's JavaScript, pinned and served out of the package"), ("1,767 icons", "Lucide, inlined by name. No icon font, no sprite request, no runtime JS."), ]) }} {{ defs("The whole surface you touch", [ ("FjkitConfig, mount_fjkit", "the wiring, once per app"), ("@render", "names the template; the handler returns data"), ]) }} {% endcall %} {% call prose() %}

Runtime dependencies: FastAPI and Jinja2.

{% endcall %} {% endcall %} {# ============================================================== 04 #} {% set i04 %}Pick the page by what you are trying to do.{% endset %} {% call lesson("next", "04", "Where to go next", i04) %} {% set n_learn %}Learn — how the app is wired, how htmx swaps work, form errors, messages, rebranding and fjkit check. Read 01–05 first.{% endset %} {% set n_components %}Components — every macro, live, with the call that produced it{% endset %} {% set n_cheatsheet %}Cheatsheet — every signature, shell block, template global and htmx attribute on one page{% endset %} {% set n_plugins %}Plugins — sign-in, flash messages, charts, an API console and an admin, and how to write your own{% endset %} {% set n_arch %}Architecture — the five closed decisions as pictures: the package boundary, eject, when the build runs, one partial with two entry points, and the gate loop{% endset %} {% set n_why %}Why fjkit — what the alternatives cost, and what this one gives up in exchange{% endset %} {{ defs("On this site", [ ("decide whether to use it at all", n_why), ("see the shape before I commit to it", n_arch), ("build my first real screen", n_learn), ("find a component", n_components), ("look up a parameter", n_cheatsheet), ("add auth, charts or an admin", n_plugins), ]) }} {% endcall %} {% endcall %} {% endblock %}