Installation
One generator wires everything, and the same generator is the upgrade path: every step is idempotent, so re-running it on a years-old app is always safe.
Install
gem "poetry" # poetry-core + poetry-ui + poetry-lucide at one version
gem "poetry-agent" # optional: MCP server exe, WebMCP, AG-UI and A2UI surfaces
# gem "poetry-charts" # optional: server-rendered SVG charts
bundle install
bin/rails g poetry:install --theme default
The installer copies the vendored CSS set under
app/assets/tailwind/poetry/ (tokens, the Tailwind theme
mapping, the animation layer, your chosen
visual theme, the class safelist, the
app-owned typeset.css), injects the Tailwind entry
imports line by line, registers the Stimulus controllers, mounts the
engine (llms.txt), adds the herb gem to your development
group (the parser behind poetry:check) and bundles it
unless Rails brings the gem already, as it does from 8.2, and
writes the AGENTS.md section and the Claude Code skills
(when your repository ignores .claude/skills the install
says so: the skills still serve that checkout, and teammates and CI
get theirs with bin/rails g poetry:skill).
Add --charts to wire
poetry-charts, and --no-preflight when your Tailwind entry
omits preflight (see
Installing into an app that already has CSS).
Vendored files refresh on every run, the four files you own
(base.css, typeset.css, the initializer,
the components manifest) are seeded once and never rewritten, and
injections append only missing lines. Before writing anything it lists every Poetry
token name your stylesheets already declare; those keep their values
(the Theming guide, "Your tokens win").
Installing into an app that already has CSS
A fresh app takes the install as written. An app with years of its own stylesheets meets three consequences of how Poetry sits in the cascade. None of them is a bug, and each has a one-line answer.
Your unlayered CSS beats the theme
Poetry's theme imports into layer(base) so that utility
classes in your templates always win over it. The other side of
that contract: any unlayered rule beats any
layered one, whatever its specificity. A legacy
a { color: … } wins over every Poetry link surface
(link Buttons, Breadcrumb, Pagination, NavigationMenu), a legacy
input { … } over every field, down to the text color
on a primary button rendered as a link. If you want Poetry's rules
to win, give your stylesheet a layer, and mind where: a layer
declared after Tailwind's import ranks above utilities and
makes things worse. Import it into layer(base) above
the Poetry lines instead: same layer, so specificity decides, and
Poetry's class selectors beat element selectors.
/* app/assets/tailwind/application.css */
@import "tailwindcss";
@import "./legacy.css" layer(base); /* your existing stylesheet, ABOVE the poetry lines */
@import "./poetry/tokens.css" layer(theme);
/* ...the poetry lines the installer wrote... */
Tokens you already own keep winning
Poetry's tokens are defaults. A --primary or
--radius your app already declares wins, and reaches
Poetry's components; the install lists every such name. The
Theming guide
has the rule and the escape hatches.
Preflight, and the app that has its own reset
@import "tailwindcss" brings Tailwind's preflight, and
the installer writes that import when your app has no Tailwind entry
yet. Poetry's components lean on a handful of preflight's
normalizations: the universal border: 0 solid and zero
padding, transparent form controls, headings at inherited size,
collapsed tables, hidden [hidden]. Measured against the
golden screenshot corpus, removing preflight changes two thirds of
it even behind a modern reset: triggers grow native button chrome,
dialogs keep their browser border and padding, titles jump in size.
So an app that imports Tailwind without preflight installs the reset
floor instead:
$ bin/rails g poetry:install --no-preflight
# app/assets/tailwind/poetry/reset.css <- preflight at zero specificity
# @import "./poetry/reset.css" layer(base); <- ahead of the theme in your entry
The floor is preflight itself, generated from the pinned Tailwind
and gated against it, with every selector wrapped in
:where(). It has zero specificity, so the floor fills
only what nothing else declared: an unlayered rule of yours always
wins, and a rule you import into layer(base) wins when
it comes after the floor's import (same layer, same zero
specificity, later wins). One deliberate exception:
[hidden] keeps preflight's !important, so
a hidden element stays hidden under any utility. On a fresh entry the flag also writes the split
no-preflight import; on an existing entry it never touches your
import lines. A plain re-run keeps the floor once installed. If an
entry imports neither preflight nor the floor, the installer and
poetry:check say so in one line.
Installing into a Jumpstart Pro app
A Jumpstart Pro app is the case above with a known answer. Its
stylesheet styles links, lists, headings and form controls for the
whole page, it declares its own primary color and page background
under names Poetry's tokens share, and its screens live in an engine
rather than in app/views. The
Jumpstart Pro installer
handles all of it in one generator: it runs this install, reconciles
Jumpstart's stylesheets with Poetry's, and installs Poetry
recreations of the app's screens as overrides.
bundle add poetry-jumpstart_pro --group development
bin/rails generate poetry_jumpstart_pro:install # runs poetry:install when needed
bin/rails tailwindcss:build
Upgrade an existing app
The install generator is the upgrade path, not a one-shot.
Gem-owned code (components, templates, controllers, the registry and
MCP surface) upgrades with bundle update alone. The
vendored CSS files and the safelist upgrade only when the generator
re-runs: new components ship their theme rules and classes there, so
skipping the re-run leaves them unstyled. Never pass
--force: the vendored files refresh on every run without
it, and the four files the install seeds for you to own (the
components manifest, the initializer, base.css,
typeset.css) are never rewritten once they exist.
bundle update poetry poetry-agent poetry-charts
bin/rails g poetry:install # your theme choice sticks; --theme switches
bin/rails tailwindcss:build # or your CSS build
bin/rails test # confirm the app still renders
A plain re-run refreshes every vendored file, the chart files
included once they are installed, and the Claude Code skills, which
are generated and are replaced without a question; pass
--skip to keep a skill file you edited by hand. It
keeps the theme already installed (it reads the
fragment header back: the slot is always
poetry/style-default.css, and its first line names the
theme that fills it); passing --theme is how you
switch. Files you own (base.css,
typeset.css, the initializer, the component manifest,
and everything copied in) are never rewritten.
The three ownership tiers
| Tier | Examples | Upgrades via |
|---|---|---|
| Gem-owned | components, controllers, registry, MCP, llms.txt | bundle update |
| Vendored into the app | app/assets/tailwind/poetry/*, safelist.txt |
bin/rails g poetry:install |
| App-owned copy-ins | poetry:add components, blocks, base.css, typeset.css, initializer |
you; poetry:diff reports |
Copy-ins: poetry:diff
Components copied in with poetry:add and blocks copied
with poetry:block are yours: nothing ever rewrites
them. Every poetry:add records the gem version it
copied from in config/poetry_components.yml, and the
read-only drift report shows where your copies stand after an
upgrade:
bin/rails g poetry:diff # read-only: copied at / gem ships / file-by-file drift
bin/rails g poetry:add button # adds newly-shipped files; never overwrites yours
Blocks are starting points meant to be edited, so the report treats a differing block as normal, not drift; it simply points at the gem's current template for reference.
Two notes from a fresh Rails 8.1 host
Neither is Poetry's, both cost an hour if unknown. Rails 8.1.3 with
the json gem at 3.0 returns 500 on every request after the first in
a process (the session cookie decode calls JSON.parse
with two arguments, which json 3 rejects); pin
gem "json", "< 3" until Rails ships the fix. And
rails g scaffold ... password:digest needs
bcrypt enabled in the Gemfile before it runs; the
generator's own fixture template references it.
Scaffold templates
Rails lets an app override its generator templates from
lib/templates/; Poetry ships a set that renders with
the library. After installing them, the standard scaffold generator
produces a DataTable index with sanitized URL state (sortable
columns, filter, pagination and a matching controller), a form on the
Poetry form builder (one f.input per attribute, the
control inferred from the column, the name and the validations), and
a MetadataList show page:
bin/rails g poetry:scaffold_templates
bin/rails g scaffold Post title:string body:text published:boolean
The copies under lib/templates/ are app-owned: edit
them freely, and re-runs never overwrite. That is also why a Simple
Form app keeps its own scaffold form partial: with the bridge active
it already renders through Poetry, and nothing here replaces it. The templates are
lint-gated in the gem's own suite, so generated views pass
poetry:check by construction.