Summary
cf-ui 0.2.0 ships 14 components across 5 themes, and they are all structural — breadcrumb, card, checkbox-group, form-field, modal, navbar, notification, pagination, panel, progress, select, table, tabs, textarea. There is no primitives layer: no button, icon, badge, heading, or standalone label.
That gap is what stops a consuming repo from going all-in. Measured against RankedJobs (108 templates, ~86 using CSS-framework classes), the five most-used constructs are button (204 occurrences), label (129), title (108), table (90), and icon (83). cf-ui covers exactly one of them. Meanwhile four components it does ship — modal, tabs, panel, breadcrumb — have zero uses in that repo.
So a repo adopting cf-ui today abstracts its rare markup and hand-writes its common markup. That is backwards, and it is why adoption has stalled at 1 component (<c-cf.notification>) after three months.
Why this belongs upstream rather than in each consumer
The house family spans two template engines — Jinja (FastAPI, Litestar) and django-cotton (Django) — and the same primitives are about to be written twice, in resume_maker and RankedJobs, in the same quarter. Cross-engine reuse is the thing cf-ui exists for, and primitives are where the duplication is highest because they are the highest-frequency markup in every repo.
It also matters for atelier, which is a production multi-tenant white-label CMS where the theme layer is a product surface rather than an internal convention. A primitives layer that a non-designer can compose without producing something broken is a feature there, not a refactor.
Proposed primitives
Tiered by measured frequency, so the first tier can ship and be useful before the rest exists.
Tier 1 — the high-frequency five
| Component |
Why |
Notes |
button |
204 uses in the reference repo; the single most common construct in every consumer |
Needs variant (primary/danger/…), size, state (loading/disabled), and a link-vs-button element choice |
icon |
83 uses |
The hardest to abstract — see open questions |
badge |
72 uses (tag/tags) |
Bulma tag → daisy badge → Bootstrap badge → Fomantic label → Foundation label |
heading |
123 uses (title/subtitle) |
Level (h1–h6) decoupled from visual size |
label |
129 uses |
Standalone. form-field already bundles a label, but most uses are outside a form field |
Tier 2 — containers
| Component |
Why |
box / surface |
59 uses. A plain bordered/elevated container; distinct from card, which imposes header/body/footer |
prose / content |
61 uses. Typographic reset for server-rendered or user-supplied HTML |
Tier 3 — layout
grid (25 columns + 28 column + 53 width tokens in the reference repo). Deliberately last, and possibly out of scope — see open questions.
Per-component cost
Following the existing layout, one component is 11 files:
templates/cotton/cf/<name>.html — engine-neutral base
templates/cotton/_themes/{bootstrap,bulma,daisy,fomantic,foundation}/<name>.html — 5
templates/jinja/{bootstrap,bulma,daisy,fomantic,foundation}/<Name>.jinja — 5
Tier 1 is therefore 55 files, Tier 2 is 22. Mechanical once the prop contract is settled — the prop contract is the actual design work, and it should be settled for all of Tier 1 at once rather than per component, because these compose with each other (a button contains an icon; a badge sits inside a heading).
Open questions
-
Icons have no cross-framework answer. Bootstrap ships Bootstrap Icons, Bulma assumes Font Awesome, daisyUI assumes nothing and defers to Tailwind + whatever SVG set you bring. There is no class-level abstraction that works across all five. Options: (a) icon takes a slot and only supplies sizing/alignment, (b) cf-ui adopts one icon set as a peer dependency, (c) icon is dropped from Tier 1. Option (a) is the least opinionated and probably right, but it delivers less than the usage count suggests.
-
Is grid in scope at all? Bootstrap, Bulma, Foundation, and Fomantic all ship 12-column grid systems with different class vocabularies; daisyUI ships none and defers entirely to Tailwind utilities. So the daisy variant of a grid component would emit raw Tailwind while the other four emit framework classes — a genuine asymmetry rather than a thin adapter. Worth deciding explicitly before building.
-
Does button need to render both <button> and <a>? Every framework styles them identically, and consumers use both. An href prop that switches the element is the obvious answer, but it interacts with the disabled state (an <a> cannot be disabled) and needs a documented rule.
-
Escaping. 0.2.0 made Jinja templates escape their own output, so any prop deliberately carrying markup must be markupsafe.Markup. Primitives take more caller-supplied content than the structural components do — prose/content exists precisely to wrap untrusted HTML. The escaping contract for these needs to be explicit in the docs, not inferred.
Acceptance criteria
Notes
Raised from the RankedJobs UI-refresh assessment (Ranked-Jobs#242). That repo is about to migrate ~86 templates from Bulma to Tailwind + daisyUI and would consume this immediately; resume_maker is the other near-term consumer on the Jinja side. Neither should build these locally — the point of the package is that the second consumer gets them free.
Frequency figures are measured from RankedJobs and are illustrative of the class of repo, not a universal ranking.
Summary
cf-ui 0.2.0 ships 14 components across 5 themes, and they are all structural — breadcrumb, card, checkbox-group, form-field, modal, navbar, notification, pagination, panel, progress, select, table, tabs, textarea. There is no primitives layer: no button, icon, badge, heading, or standalone label.
That gap is what stops a consuming repo from going all-in. Measured against RankedJobs (108 templates, ~86 using CSS-framework classes), the five most-used constructs are
button(204 occurrences),label(129),title(108),table(90), andicon(83). cf-ui covers exactly one of them. Meanwhile four components it does ship — modal, tabs, panel, breadcrumb — have zero uses in that repo.So a repo adopting cf-ui today abstracts its rare markup and hand-writes its common markup. That is backwards, and it is why adoption has stalled at 1 component (
<c-cf.notification>) after three months.Why this belongs upstream rather than in each consumer
The house family spans two template engines — Jinja (FastAPI, Litestar) and django-cotton (Django) — and the same primitives are about to be written twice, in resume_maker and RankedJobs, in the same quarter. Cross-engine reuse is the thing cf-ui exists for, and primitives are where the duplication is highest because they are the highest-frequency markup in every repo.
It also matters for atelier, which is a production multi-tenant white-label CMS where the theme layer is a product surface rather than an internal convention. A primitives layer that a non-designer can compose without producing something broken is a feature there, not a refactor.
Proposed primitives
Tiered by measured frequency, so the first tier can ship and be useful before the rest exists.
Tier 1 — the high-frequency five
buttoniconbadgetag/tags)tag→ daisybadge→ Bootstrapbadge→ Fomanticlabel→ Foundationlabelheadingtitle/subtitle)labelform-fieldalready bundles a label, but most uses are outside a form fieldTier 2 — containers
box/surfacecard, which imposes header/body/footerprose/contentTier 3 — layout
grid(25columns+ 28column+ 53 width tokens in the reference repo). Deliberately last, and possibly out of scope — see open questions.Per-component cost
Following the existing layout, one component is 11 files:
templates/cotton/cf/<name>.html— engine-neutral basetemplates/cotton/_themes/{bootstrap,bulma,daisy,fomantic,foundation}/<name>.html— 5templates/jinja/{bootstrap,bulma,daisy,fomantic,foundation}/<Name>.jinja— 5Tier 1 is therefore 55 files, Tier 2 is 22. Mechanical once the prop contract is settled — the prop contract is the actual design work, and it should be settled for all of Tier 1 at once rather than per component, because these compose with each other (a button contains an icon; a badge sits inside a heading).
Open questions
Icons have no cross-framework answer. Bootstrap ships Bootstrap Icons, Bulma assumes Font Awesome, daisyUI assumes nothing and defers to Tailwind + whatever SVG set you bring. There is no class-level abstraction that works across all five. Options: (a)
icontakes a slot and only supplies sizing/alignment, (b) cf-ui adopts one icon set as a peer dependency, (c) icon is dropped from Tier 1. Option (a) is the least opinionated and probably right, but it delivers less than the usage count suggests.Is
gridin scope at all? Bootstrap, Bulma, Foundation, and Fomantic all ship 12-column grid systems with different class vocabularies; daisyUI ships none and defers entirely to Tailwind utilities. So the daisy variant of agridcomponent would emit raw Tailwind while the other four emit framework classes — a genuine asymmetry rather than a thin adapter. Worth deciding explicitly before building.Does
buttonneed to render both<button>and<a>? Every framework styles them identically, and consumers use both. Anhrefprop that switches the element is the obvious answer, but it interacts with the disabled state (an<a>cannot be disabled) and needs a documented rule.Escaping. 0.2.0 made Jinja templates escape their own output, so any prop deliberately carrying markup must be
markupsafe.Markup. Primitives take more caller-supplied content than the structural components do —prose/contentexists precisely to wrap untrusted HTML. The escaping contract for these needs to be explicit in the docs, not inferred.Acceptance criteria
button,badge,heading,label, andiconsubject to Q1) implemented as base + 5 cotton themes + 5 jinja themes each.axes.py— a bad value fails the build, not review.Notes
Raised from the RankedJobs UI-refresh assessment (Ranked-Jobs#242). That repo is about to migrate ~86 templates from Bulma to Tailwind + daisyUI and would consume this immediately; resume_maker is the other near-term consumer on the Jinja side. Neither should build these locally — the point of the package is that the second consumer gets them free.
Frequency figures are measured from RankedJobs and are illustrative of the class of repo, not a universal ranking.