Asking AI to query Anvil Open-Source Code - Blew my mind

Hi guys,

not sure if this is stuff others are doing already, but it blew my mind!

I’ve added the anvil source code to my coding IDE and now whenever I’m unsure about some deep technical stuff within anvil I ask the ai to read through the source code and get me an answer. And it is working!

Examples:

  1. Is it really not possible for the same timer to be running in parallel?

AI Answer: From Timer.tsx, the Anvil Timer uses:

raiseEventAsync({}, self, "tick").finally(function() { self._anvil.setTimer(); })
  1. Is q.any_of() always faster than q.none_of()?

These kind of things.

Feels like a cheat-code, to be honest ^^ So thought I would share :smiley:

Cheers,
David

5 Likes

I have the ChatGPT Teams plan, which gives me plenty of Codex time.

I used to use Codex CLI, but I recently switched to OpenCode connected to my account, no need for API key. The Codex desktop app is available on Mac, when it will be available on Windows I will probably go that way.

I find these tools easier to use for generic exploration of the codebase than the IDE.

I’ve given OpenCode+Codex even to non developers to work on our internal wiki. Trainees use it to ask questions, much better than digging in the wiki in the traditional way, and maintainers love it too. I’ve downloaded ~1000 pages and asked things like “The truth about the estimating process is in page X. Find all pages with redundant or contradictory information and create a report. Reorganize the whole process into a new set of pages including the truth and any additional information from other pages.” The result was amazing, we may end up with a wiki without stale or contradicting information. One ancient dream of mine finally coming true!

1 Like

Yeah I have found great success having the source code of all my dependencies locally (I need that anyway to run the app locally). I have had Claude Code explain a lot of stuff in m3, anvil-extras, routing dependencies, etc. just by reading the source code.

I no longer drag and drop in Designer - the AI (personally I think Google is best, just above ChatGPT) builds complex forms with dropdowns, buttons, etc and pro-level css roles for me - incredible

1 Like

Cool! Do you let the AI build custom html forms, or blank panels and it inserts anvil components?

the basic idea is like:

# Top Navigation Bar
nav = FlowPanel(background="#1E293B",align="justify")

title_label = Label(text="ANONYMIZED EXECUTIVE VIEW", foreground="#F8FAFC", font_size=20, bold=True)
nav.add_component(title_label)

# Filter Dropdown
self.drop_range = DropDown(
  items=["Last 30 Days", "Last 90 Days", "Year to Date"],
  selected_value="Last 30 Days",
  foreground="#F8FAFC",
  background="#334155"
)
self.drop_range.set_event_handler("change", self.refresh_dashboard)

btn_export = Button(text="EXPORT CSV", role="secondary", icon="fa:download")
btn_export.set_event_handler("click", lambda **e: Notification("Exporting Data...").show())

controls = FlowPanel(spacing="medium")
controls.add_component(self.drop_range)
controls.add_component(btn_export)

nav.add_component(controls)
self.add_component(nav)

or like:

chart_grid = GridPanel()

# 1. Main Trend Line
self.plot_trend = Plot()
self.plot_trend.layout = go.Layout(
  title="Revenue Trend",
  paper_bgcolor='rgba(0,0,0,0)',
  plot_bgcolor='rgba(0,0,0,0)',
  font=dict(color='#94A3B8'),
  xaxis=dict(showgrid=False),
  yaxis=dict(gridcolor='#334155'),
  margin=dict(t=40, b=40, l=40, r=40)
)
self.plot_trend.data = [go.Scatter(
  x=self.mock_data['dates'], 
  y=self.mock_data['sales'],
  line=dict(color='#38BDF8', width=3),
  fill='tozeroy'
)]

# 2. Category Pie
self.plot_pie = Plot()
self.plot_pie.layout = go.Layout(
  title="Sales by Category",
  paper_bgcolor='rgba(0,0,0,0)',
  plot_bgcolor='rgba(0,0,0,0)',
  font=dict(color='#94A3B8'),
  margin=dict(t=40, b=40, l=40, r=40),
  showlegend=False
)
self.plot_pie.data = [go.Pie(
  labels=self.mock_data['cat_labels'],
  values=self.mock_data['cat_shares'],
  hole=0.5,
  marker=dict(colors=['#38BDF8', '#34D399', '#FB7185', '#A78BFA', '#FBBF24'])
)]

chart_grid.add_component(self.plot_trend, row="A", col_xs=0, width_xs=12, width_md=8)
chart_grid.add_component(self.plot_pie, row="A", col_xs=0, width_xs=12, width_md=4)

self.add_component(chart_grid)

In my experience, Google’s models are consistently very good at the beginning. The first few turns can be impressive. The problem starts later, when you move into advanced development or long-term maintenance. When you need precise, incremental changes inside an existing codebase, they quickly become a waste of time.

ChatGPT is less creative by default, but much more controllable. If I want something polished, I have to describe it carefully. But once I do, it follows instructions very closely. It can change exactly what I ask, fix specific issues, or leave existing problems untouched if that’s what I want.

The difference isn’t just a tendency, it’s architectural. Gemini-style tooling rewrites whole files as part of how it operates. That means if you ask to change one line in a 5,000-line file, you often get dozens of unrelated changes. It feels like Google is trying to take advantage of their huge context window instead of focusing strictly on the specific change requested. That approach is great if you want to understand or generate something at the scale of a whole book, but in large codebases it increases the risk of unintended edits.

For example, I have some old sorting functions written in VBA. Every time Gemini touched the file containing them, even when the requested change was 1,000 lines away, it would rewrite those functions because they didn’t match its idea of a “standard” implementation. That kind of behavior makes it hard to trust during maintenance work.

Codex-style tooling applies targeted edits in specific locations. It modifies what you asked for and leaves the rest alone. For maintenance work, that makes a huge difference.

1 Like

When I start a new app (which doesn’t happen often lately, I only created a small one last week), I usually add a form with one or two basic components and use that as the starting point for Codex. If I let it begin completely from scratch, it tends to default to patterns that feel more Django than Anvil. Providing a minimal structure helps it stay aligned with how I build apps.

Along with that, I use a small example app and a simple AGENTS.md file. Mine is still very minimal because most of my work is maintenance on existing projects. In maintenance mode, the model generally sticks to the established style of the app. If I notice it drifting, I just add another line to AGENTS.md to steer it back.

When I start building new apps more regularly, I’ll likely expand AGENTS.md with more detailed guidance. For now, this is roughly what I use:

Notes about Anvil app development
- Follow Anvil best practices and use the layout system.
- Built-in Anvil APIs only (see https://anvil.works/docs/api/anvil).
- Avoid using JavaScript when there is a Python alternative, for example, use js.get_dom_node to access the DOM, not javascript.
1 Like

What do you use for ‘Codex-style tooling’ precisely then?

In my opinion shaped by Google AI Studio, it can handle immense code bases, so I will literally open a new Playground, paste in Server Code, then UI Code, then whatever else, and tell it from scratch what I want to change. If it gets off track I refresh, re-paste and start over - overall I continue to be impressed… and have not had consistent issues with it changing code on me unawares.

Yes once I had a good app built - I also fed it to the AI as a starting template - saves many hours… can you be more specific about “Codex” and Agents.md files? this is new to me…

  • Gemini is Google’s family of large language models.
  • GPT is OpenAI’s family of large language models.
  • ChatGPT is OpenAI’s conversational interface built on top of GPT models.
  • Codex is OpenAI’s coding-focused agent built on top of GPT models. It can read, analyze, and edit files in your project using local tools.
  • AGENTS.md / GEMINI.md are project-level instruction files placed in your repository. The coding agent reads its respective file at the start of a session and follows those instructions while working on your code.
  • Gemini CLI is a terminal application you run inside a project folder. It gives Gemini access to the files in that folder and typically operates at the full-file level when applying changes.
  • Codex CLI is OpenAI’s coding agent tool for the terminal. It analyzes the codebase and applies targeted, precise changes instead of rewriting entire files unnecessarily.
  • OpenCode is an open-source AI coding agent you run from the terminal. It lets you work with whichever model you prefer. It has recently replaced Codex CLI in my daily workflow.
3 Likes

do you have codex cli integrated with anvil or are you also copy-pasting back and forth? (i feel like i have become a copy-paste monkey these days…)

With Codex CLI or OpenCode you give AI access to your drive.

I ask “add a button to do XYZ”, and they will modify the form, add the code, create the server module, create the role, etc for you. One prompt triggers the modification or creation of multiple files.

Then I use PyCharm to check the changes. If I like them, I commit and push, then I test the app. PyCharm has a great diff viewer and git interface, so you didn’t even need to learn how to use git.

I would be very interested in seeing your workflow using AI with Anvil. Would you consider making a YouTube video showing your workflow on using AI agents to make and modify anvil forms. I think it would benefit developers and Anvil greatly!!!

1 Like

I have these kind of files that i develop with claude code so it knows how i like it. Interesting to see @CyCy that you have it build the form from scratch in code. I have it create the .yaml file with specific settings (like no spacing above and below and stuff like that) and then add roles and attack those in CSS. Not sure which is better tbh.

As an example, this is my frontend dev guide, which is still in development! I’m also not certain it’s the best way to use AI for frontend in Anvil and I havent read all of it cause I just always tell the ai after im done with a new task to rewrite the file with the new best-practice learnings:

AnkiBuddy Frontend Development Guide

This doc = practical reference. For CSS/JS/Python philosophy and rules, see responsive-styling.md.
Goal: Give an AI (or developer) enough context to one-shot a new component without rediscovering Anvil quirks.


1. Standard Reusable Roles & Classes

ALWAYS check this catalog first. Reuse an existing role before creating a new one.
If none fit, create a new role using the design tokens from §2.
Roles marked (niche) are used in one specific context; all others are general-purpose.

Button Roles

Every button needs a role. Pick from this catalog.

primary-button — High Emphasis CTA

  • Look: Solid blue background, white text, pill shape
  • Use for: Main actions — “Registrieren”, “Speichern”, “Erinnern”
  • YAML: {role: primary-button, text: Speichern}

secondary-button — Medium Emphasis

  • Look: Light blue tonal background (--primary-10), dark text, pill shape
  • Use for: Alternative actions — “Einloggen”, “Abbrechen”, gift button
  • YAML: {role: secondary-button, text: Einloggen}

text-button — Low Emphasis

  • Look: No background, primary blue text, transparent
  • Use for: Utility actions — “Mehr erfahren”, referral link
  • YAML: {role: text-button, text: Mehr erfahren}
  • Note: Has negative margin on first/last child in FlowPanel for optical edge alignment

icon-button — Icon-Only Circular

  • Look: Transparent circle, icon centered via absolute positioning
  • Use for: Close ✕, settings, any icon-only action
  • YAML: {role: icon-button, icon: '_/theme/Icons/lc_x.svg', text: '', tooltip: Schließen}
  • Compose with: secondary-icon-button for tinted circle, lc-card-button for white pill

secondary-icon-button — Tinted Circle (compose with icon-button)

  • Look: --primary-10 circle background
  • YAML: {role: [icon-button, secondary-icon-button], icon: ...}

lc-card-button — White Pill with Shadow (niche)

  • Look: White background, subtle shadow, pill shape (border-radius: 100px)
  • Icon variant: {role: [icon-button, lc-card-button], icon: '_/theme/Icons/lc_edit.svg'}
  • Text variant: {role: lc-card-button, text: 'Antwort verstecken'} — auto-detects via :not(.anvil-role-icon-button) and adds padding/color/font
  • State modifiers (compose via array):
    • lc-highlight — heavy shadow to draw attention: [icon-button, lc-card-button, lc-highlight]
    • lc-approved — green surface for approved cards: [icon-button, lc-card-button, lc-approved]
    • lc-warning — error color + full opacity when disabled: [lc-card-button, lc-warning]
  • Python toggle example: self.btn.role = "lc-card-button" if ok else ["lc-card-button", "lc-warning"]

appbar-button — Responsive Appbar (niche)

  • Look: 14px desktop, 11px mobile
  • Use for: Header bar buttons (referral link)

Icon Conventions

Source Format When
Font Awesome fa:clock-o Generic/universal icons
Custom SVG _/theme/Icons/lc_*.svg Branded/domain-specific icons
No icon icon: '' or omit Text-only buttons

SVG naming: lc_ prefix = learning card icons. All use stroke="#354052" (dark slate), stroke-width="2", stroke-linecap="round". For white-on-colored-bg icons: stroke="#FFFFFF".


Typography Roles

Apply these to Labels. No media queries needed — font-size tokens handle responsiveness.

Role Font Size Weight Color Use
type-hero 48→32px bold --primary Landing page headline
type-lead 36→24px normal --text-tertiary Landing page subheading
type-title 20→18px bold inherited Section headers, deck names
type-subtitle 16px medium --on-surface Panel headers, form titles
type-body 14px normal inherited Body text (DEFAULT)
type-caption 12px normal --text-tertiary Captions, hints, secondary

YAML: {role: type-title, text: 'Section Title'}


Card Roles

Role Look Use
outlined-card Border, surface bg, 12px radius Default cards
elevated-card Shadow, surface bg, 12px radius Prominent cards
tonal-card Surface-variant bg, 12px radius Subtle/grouped content

Form Input Roles

Role Look Use
outlined Transparent bg, outline border, 4px radius Standard text input
outlined-error Error color border Validation error state
input-error Error border on default bg Validation error (filled)
form-dropdown (niche) Styled select with custom arrow, F5F7F9 bg User profile popup
form-textbox (niche) Styled input, F5F7F9 bg, 8px radius User profile popup

Link Roles

Role Look Use
link-tertiary 12px, --text-tertiary, underline on hover Footer links, de-emphasized

Visibility Roles

Use these for responsive show/hide. Applied to any component.

Role Effect
hide-at-mobile-horizontal-and-smaller Hidden ≤768px, visible above
hide-at-tablet-and-smaller Hidden ≤992px, visible above
hide-at-ultrawide-and-smaller Hidden ≤1280px, visible above
show-at-mobile-horizontal-and-smaller Visible ≤768px, hidden above
show-at-mobile-vertical-only Visible ≤480px, hidden above

These automatically hide the .flow-panel-item wrapper via :has() when inside FlowPanels.


Layout Utility Roles

Role Effect Use
shadow-escape overflow: visible on .anvil-panel-col ColumnPanel children with box-shadows
no-wrap flex-wrap: nowrap on .flow-panel-gutter FlowPanel rows that shouldn’t wrap
text-nowrap text-wrap: nowrap on element + spans Prevent text wrapping
rich-text-wrap white-space: normal + word-break Allow RichText wrapping
column-panel-max-w-730 max-width: 730px Content width constraint

Other Standard Roles

Role Component Look Use
separator Label 1px horizontal line Section dividers (set text: '')
btn-transparent (niche) Button Transparent bg, tight padding Referral form variants
popup-section-header (niche) Label 18px bold, #212121 User profile popup headers
popup-field-label (niche) Label 16px bold, 70% opacity User profile field labels
popup-section-divider (niche) Label 1px border-bottom User profile section dividers
popup-privacy-box (niche) ColumnPanel Subtle gray bg, 8px radius Privacy message container
pdf-upload-alert (niche) Alert 400px max-width, white bg PDF upload confirmation

3. Component How-To Guides

Why this section exists: Anvil wraps every component in layers of HTML divs, adds
Bootstrap 3 classes, injects inline styles, and applies default spacing. You can’t just
write CSS targeting your component — you must understand Anvil’s internal DOM structure
and work with (or around) it. Each component type has its own quirks.

This section documents each component’s real DOM output and the standard CSS patterns
for styling them. The “Gotchas” lists are the Anvil-specific traps we’ve already hit;
the workarounds are proven.

ColumnPanel

What it is: Vertical stacking container. Each child is wrapped in .anvil-panel-section.

Anvil renders:

div.anvil-role-{role} .column-panel
 └── div.anvil-panel-section          ← one per child
      └── div.anvil-panel-section-container
           └── div.anvil-panel-section-gutter
                └── div.anvil-panel-row
                     └── div.anvil-panel-col (overflow-x: hidden!)
                          └── div.col-padding.col-padding-{col_spacing}
                               └── [YOUR COMPONENT]

Standard YAML:

name: pnl_my_panel
properties: {col_spacing: none, role: my-panel}
type: ColumnPanel
layout_properties: {spacing_above: none, spacing_below: none}

CSS — spacing between children:

/* Adjacent sibling margin pattern (established convention) */
.anvil-role-my-panel > .anvil-panel-section + .anvil-panel-section {
    margin-top: var(--gap-component);  /* or --gap-element, --section-gap */
}

When to use col_spacing: none: Always, when you want CSS control. Default is medium (15px padding on every column wrapper). Setting none gives you 0 1px padding — essentially zero.

Common use cases:

Pattern CSS When
Vertical stack with gaps Adjacent sibling margins (above) Most common — form sections, card content
Flex column with gap display: flex; flex-direction: column; gap: ... on .column-panel When you need flex features (e.g., flex-grow on a child, equal-height items)
Simple wrapper No CSS needed Just grouping children for visibility toggling

Gotchas:

  • .anvil-panel-col has overflow-x: hidden by default. Add overflow: visible if children need to escape (shadows, dropdowns). Use the shadow-escape role.
  • Every child component gets anvil-spacing-above-small by default (4px margin). Set spacing_above: none in layout_properties, or use a broad reset (see §4).

FlowPanel

What it is: Horizontal flex container. Children are wrapped in .flow-panel-item.

Anvil renders:

div.anvil-role-{role} .flow-panel .flow-spacing-{gap} .vertical-align-{vertical_align}
 └── div.flow-panel-gutter (ACTUAL flex container)
      ├── div.flow-panel-item (flex: 0 1 auto)
      │    └── [COMPONENT 1]
      └── div.flow-panel-item
           └── [COMPONENT 2]

Standard YAML:

name: pnl_my_row
properties: {align: left, gap: none, role: my-row, vertical_align: middle}
type: FlowPanel
layout_properties: {spacing_above: none, spacing_below: none}

CSS:

.anvil-role-my-row > .flow-panel-gutter {
    gap: var(--gap-element);
}

Key YAML properties and what they control:

YAML Renders as Notes
gap: none .flow-spacing-none Always use. Disables Anvil’s margin system so CSS gap works
align: justify inline justify-content: space-between Inline style — can’t override without !important
align: left inline justify-content: flex-start Use YAML for this, not CSS
vertical_align: middle .vertical-align-middlealign-items: center Can use YAML (sets class) or CSS

Gotchas:

  • The role wrapper is display: block, NOT flex. The div.anvil-role-{role} element has display: block. CSS gap, align-items, justify-content on the role selector do nothing. Always target > .flow-panel-gutter for any flex property. Same for child selectors: use > .flow-panel-gutter > .flow-panel-item, never > .flow-panel-item.
  • Always set gap: none. Without it, Anvil adds margins to .flow-panel-item that conflict with CSS gap.
  • align is an inline style. Anvil injects justify-content directly on the gutter. Use YAML to set it, not CSS.
  • Items don’t stretch by default. .flow-panel-item has flex: 0 1 auto. To stretch a child (e.g., input filling space), target the wrapper: .flow-panel-gutter > .flow-panel-item:first-child { flex: 1; min-width: 0; }.
  • Hiding children: Must also hide the .flow-panel-item wrapper, or it takes up gap space. Use :has(): .flow-panel-item:has(> .anvil-role-hidden) { display: none; }.
  • flex-wrap: FlowPanels wrap by default. Add flex-wrap: nowrap on the gutter if you need a non-wrapping row (e.g., header with close button). Or use the no-wrap utility role.

GridPanel

What it is: Bootstrap 3 grid (12-column). Children placed in rows/columns.

Anvil renders:

div.anvil-role-{role} .grid-panel
 └── div.row (style="margin-bottom: 10px")     ← inline JS!
      ├── div.col-xs-6.col-sm-6.col-md-6.col-lg-6 (padding: 0 15px)
      │    └── [BUTTON 1]
      └── div.col-xs-6...
           └── [BUTTON 2]
 └── div.row (style="margin-bottom: 10px")
      └── ...

Standard YAML:

name: pnl_my_grid
properties: {role: my-grid}
type: GridPanel
layout_properties: {spacing_above: none, spacing_below: none}

# Children specify position:
- name: btn_top_left
  layout_properties: {col_xs: 0, row: ROW1, width_xs: 6}
  type: Button

CSS — compact gutters:

.anvil-role-my-grid {
    overflow: visible;
}
.anvil-role-my-grid > .row {
    margin-left: calc(var(--gap-element) / -2);
    margin-right: calc(var(--gap-element) / -2);
    margin-bottom: var(--gap-element) !important;  /* override inline JS */
}
.anvil-role-my-grid > .row:last-child {
    margin-bottom: 0 !important;
}
.anvil-role-my-grid > .row > [class*="col-"] {
    padding-left: calc(var(--gap-element) / 2);
    padding-right: calc(var(--gap-element) / 2);
    overflow: visible;
}

Gotchas:

  • Row margin-bottom is inline JS (default 10px from row_spacing property). Requires !important to override.
  • Bootstrap default gutters are 15px. Must override both row negative margins and column padding.
  • .anvil-panel-col has overflow-x: hidden — add overflow: visible if children have shadows/borders that get clipped.
  • Use .row selector, not [data-anvil-gridpanel-row] — the data attribute selector doesn’t match in practice.

Button

Anvil renders:

div.anvil-role-{role} .anvil-button .anvil-inlinable
 └── button.btn.btn-default (style="max-width:100%; overflow:hidden")
      ├── i.anvil-component-icon.left.left-icon
      │    └── img (style="height:1em; vertical-align:text-bottom")
      ├── span.button-text
      └── i.anvil-component-icon.right.left-icon (display:none)  ← DUPLICATE
           └── img

Standard YAML (text button):

name: btn_action
properties: {role: primary-button, text: Do Something}
type: Button
event_bindings: {click: btn_action_click}
layout_properties: {spacing_above: none, spacing_below: none}

Standard YAML (icon button):

name: btn_close
properties: {role: icon-button, icon: '_/theme/Icons/lc_x.svg', text: '', tooltip: Schließen}
type: Button

Standard YAML (text + right icon):

name: btn_submit
properties: {role: primary-button, icon: '_/theme/Icons/lc_arrow_right.svg', icon_align: right, text: Erinnern}
type: Button

… (had to cut for length in the post) …

Image

Anvil renders:

div.anvil-role-{role} .anvil-image (style="height:Xpx; overflow:hidden")
 └── img (style="height:100%; width:100%; max-width:100%; max-height:100%")

Gotchas:

  • Anvil always injects height: 100% on the <img> — cannot be disabled via YAML. Override with !important:
    .anvil-role-my-container img {
        height: 16px !important;
        width: auto !important;
    }
    
  • display_mode controls object-fit: shrink_to_fitcontain, zoom_to_fillcover, fill_width → width:100% only.

Timer

No HTML output. Invisible component. Just triggers tick events at the specified interval (seconds). Set interval: 0 to disable.

name: timer_poll
properties: {interval: 0}
type: Timer
event_bindings: {tick: timer_poll_tick}
layout_properties: {spacing_above: none, spacing_below: none}

4. Standard Utility Roles

These roles go in theme.css as reusable solutions to common Anvil fights.
Instead of writing the same CSS workaround per-component, compose the utility role in YAML.

compact-grid — Override Bootstrap 15px Gutters

Problem: GridPanel uses Bootstrap’s 15px column padding. For card grids and compact layouts, this is too much.

CSS (add to theme.css):

.anvil-role-compact-grid {
    overflow: visible;
}
.anvil-role-compact-grid > .row {
    margin-left: calc(var(--gap-element) / -2);
    margin-right: calc(var(--gap-element) / -2);
    margin-bottom: var(--gap-element) !important;
}
.anvil-role-compact-grid > .row:last-child {
    margin-bottom: 0 !important;
}
.anvil-role-compact-grid > .row > [class*="col-"] {
    padding-left: calc(var(--gap-element) / 2);
    padding-right: calc(var(--gap-element) / 2);
    overflow: visible;
}

Usage:

properties: {role: [my-timing-grid, compact-grid]}
type: GridPanel

Effect: Uniform --gap-element (8px) gap between all grid cells, both horizontal and vertical.


stretch-first — First FlowPanel Child Fills Remaining Space

Problem: In a FlowPanel with an input + button, the input’s .flow-panel-item wrapper has flex: 0 1 auto — it doesn’t stretch. Setting flex: 1 on the input alone doesn’t help because the wrapper constrains it.

CSS (add to theme.css):

.anvil-role-stretch-first > .flow-panel-gutter > .flow-panel-item:first-child {
    flex: 1;
    min-width: 0;
}

Usage:

properties: {align: left, gap: none, role: [my-email-row, stretch-first], vertical_align: middle}
type: FlowPanel

Effect: First child fills all available space; second child (e.g., button) stays at its natural width.


reset-spacing — Zero Anvil Default Margins

Problem: Every component inside a ColumnPanel gets anvil-spacing-above-small (4px margin) by default. When you control spacing via section gaps, these margins compound and create “weird spacing.”

CSS (add to theme.css):

.anvil-role-reset-spacing .anvil-component {
    margin: 0;
}

Usage:

properties: {col_spacing: none, role: [my-panel, reset-spacing]}
type: ColumnPanel

How it works: Anvil’s spacing classes (.anvil-spacing-above-small) have 1-class specificity (0,1,0). Our selector .anvil-role-reset-spacing .anvil-component has 2-class specificity (0,2,0) — it wins.

Limitation: Does NOT work for <input> elements. Theme.css has input-specific spacing rules like input.anvil-component.anvil-spacing-below-small with element + 2-class specificity (0,2,1), which beats our selector. For inputs, you still need margin: 0 !important on the input role.


Composable Roles — Best Practice

Roles compose via arrays. Anvil applies every role in the array as a CSS class on the same wrapper element (div.anvil-role-X.anvil-role-Y). This means each role’s CSS rules stack — use this to build up component styling from reusable pieces instead of creating monolithic one-off roles.

Pattern: base + variant + modifier. Structure roles in layers:

  1. Base role — core surface/shape (e.g. lc-card-button = white pill with shadow)
  2. Variant role — changes the rendering mode (e.g. icon-button = icon-only circular)
  3. Modifier role — toggles visual state (e.g. lc-warning, lc-approved, lc-highlight)
# Icon card button (base + variant)
role: [icon-button, lc-card-button]

# Icon card button with approval state (base + variant + modifier)
role: [icon-button, lc-card-button, lc-approved]

# Text card button (base only — no icon-button variant)
role: lc-card-button

# Text card button with warning state (base + modifier)
role: [lc-card-button, lc-warning]

Python toggles modifiers at runtime by swapping the role array:

# Toggle warning state
self.btn.role = "lc-card-button" if ok else ["lc-card-button", "lc-warning"]

CSS detects variants using :not() on sibling roles. Since all roles are classes on the same element, CSS can condition on presence/absence:

/* Base — always applies */
.anvil-role-lc-card-button > button.btn { background: white; }

/* Text variant — only when icon-button is NOT also applied */
.anvil-role-lc-card-button:not(.anvil-role-icon-button) > button.btn { padding: 8px 16px; }

/* Modifier — independent of variant */
.anvil-role-lc-warning > button.btn:disabled { color: var(--error); }

Never create monolithic roles that bake variant + state into one name (e.g. my-button-disabled-warning). Instead, keep each concern in its own role so it can be independently reused and combined.

Layout utility roles

Utility roles handle generic Anvil workarounds. Put the semantic role first, utility roles after:

# ColumnPanel with CSS-controlled spacing
properties: {col_spacing: none, role: [reminder-expanded, reset-spacing]}

# GridPanel with compact gutters
properties: {role: [reminder-timing-grid, compact-grid]}

# FlowPanel with stretching input
properties: {gap: none, role: [reminder-email-row, stretch-first], vertical_align: middle}

The component-specific CSS (borders, colors, etc.) uses .anvil-role-reminder-*. The utility roles handle the generic Anvil workarounds.


5. The Anvil Tax — Quick Reference

Why we fight Anvil: Anvil generates HTML with Bootstrap 3 defaults, injects inline
styles via JavaScript, and applies framework CSS that conflict with modern design patterns.
Every component gets default 4px spacing, buttons get max-width: 100%, grids have 15px
gutters, inputs have extra margins, and images always get height: 100%. The table below
documents these defaults and our standard overrides, so you don’t rediscover them.

YAML Boilerplate Cheat Sheet

Every component type has standard YAML properties you should set:

Component Standard Properties Why
ColumnPanel col_spacing: none, role: my-panel Zero padding, enable CSS control
FlowPanel gap: none, align: {left|justify|...}, role: my-row, vertical_align: middle Disable margin system, set justify-content, enable CSS gap
GridPanel role: my-grid Enable CSS gutter overrides
All children layout_properties: {spacing_above: none, spacing_below: none} Zero Anvil’s default 4px margins

Inline Styles Anvil Injects

Component Property Inline Style Can YAML Control? Override Strategy
FlowPanel align justify-content: ... Yes Use YAML
GridPanel rows row_spacing margin-bottom: 10px No useful values !important
Button max-width: 100% No Already fixed globally
Image <img> height: 100% No !important
TextBox font props font-size, font-weight, etc. Yes Use YAML or CSS role

CSS Resets You’ll Likely Need

Situation Rule Why
TextBox in FlowPanel margin: 0 !important on input.anvil-role-* Bootstrap .form-control margin-bottom + theme.css input spacing
Components in ColumnPanel .anvil-role-panel .anvil-component { margin: 0; } Anvil default anvil-spacing-above-small = 4px
GridPanel gutters margin-left/right on .row, padding-left/right on [class*="col-"] Bootstrap default 15px
GridPanel row spacing margin-bottom: ... !important on .row Inline JS from row_spacing
Link underline Target .link-text not <a> text-decoration doesn’t propagate to block children
ColumnPanel column overflow overflow: visible on [class*="col-"] or use shadow-escape role .anvil-panel-col has overflow-x: hidden

Anvil Spacing Classes Reference

YAML Value Class Added Margin
none anvil-spacing-above-none 0px
small (default) anvil-spacing-above-small 4px
medium anvil-spacing-above-medium 8px
large anvil-spacing-above-large 16px

6. Common Pitfalls

Hard-won lessons from building components. Each one cost us at least one round-trip.

Don’t hardcode colors or sizes

Always use design tokens. #666666var(--text-secondary). 8px gap → var(--gap-element).

Don’t target broad icon selectors

.anvil-component-icon matches BOTH the visible .left and the hidden .right duplicate. Always specify .left or .right:

/* Un-hides the duplicate */
.my-btn .anvil-component-icon { display: inline-block; }
/* Targets only the visible one */
.my-btn .anvil-component-icon.left { ... }

Don’t forget the .flow-panel-item wrapper

Setting flex: 1 on a child inside a FlowPanel does nothing if the wrapper has flex: 0 1 auto. Target the wrapper.

Don’t put layout properties in container properties:

spacing_above, spacing_below are layout properties (how parent positions the child). Setting them on the container’s own properties: block creates “Orphaned Container Properties” warnings:

# Orphaned — GridPanel doesn't own spacing_above
properties: {spacing_above: none, role: my-grid}
# Correct — layout_properties tells the parent
layout_properties: {spacing_above: none, spacing_below: none}

Always check for !important necessity

Before using !important, check:

  1. Is it an Anvil inline style? (justify-content, margin-bottom, height:100%) → !important justified
  2. Is it a specificity issue? → increase specificity instead
  3. Document why in a CSS comment

SVG icons in <img> tags can’t be recolored

Anvil renders icon property as <img src="...">, not inline SVG. CSS color doesn’t work. Options:

  • Edit the SVG file’s stroke/fill color
  • Use filter: brightness(0) invert(1) for white
  • Use opacity for muting

scroll_into_view() after visibility changes

After toggling visible = True on a component, call component.scroll_into_view(smooth=True) to ensure it’s in the viewport. Anvil API: scroll_into_view(smooth=False, align="center").

Inputs always need margin: 0 !important

The reset-spacing utility role works for most components but NOT for <input> elements. Theme.css has input-specific spacing rules with higher specificity. Always add margin: 0 !important to input role CSS.

Logic-driven style changes: toggle CSS classes from Python

Best practice: When a style change is driven by application logic (loading states, error states, mode switches) rather than screen size, toggle CSS classes from Python via dom_nodes. CSS media queries handle responsive layout; Python class toggles handle behavioral state.

classList is a standard DOM API (it’s a DOMTokenList, i.e. an array-like). Use .add(), .remove(), .toggle(), .contains().

# Show loading state — disable scroll
self.skeleton_loader.visible = True
self.dom_nodes['my-container'].classList.add('no-scroll')

# Hide loading state — re-enable scroll
self.skeleton_loader.visible = False
self.dom_nodes['my-container'].classList.remove('no-scroll')
/* CSS — simple class toggle */
#my-container.no-scroll { overflow: hidden; }

Don’t try to derive behavioral state purely from CSS (e.g. :has() to detect child presence, sibling selectors to infer visibility). These approaches are fragile — Anvil’s display: none leaves elements in the DOM, custom components don’t propagate roles, and selector specificity battles with the framework. If Python already knows the state, let Python tell CSS directly via a class.

5 Likes

@davidtopf2, I initially tried starting with an automatically generated full description of the app, but it was too long and packed with details I didn’t like. So I switched to a sample app + a minimal AGENTS.md , and I just add small steering corrections as I go. Kudos to you for sticking with the more structured approach and patiently maintaining it over time, that takes discipline.

@CyCy and @jameswpierce, I have the feeling you’re thinking about developing with AI as “ask ChatGPT how to do something, copy the answer, paste it into your editor.” That’s not what we are describing.

What we are talking about is cloning the Anvil app to the local drive, running an AI agent inside the repo and letting it modify the actual codebase directly, no chatbot UI, no copy/paste loop. It works on the project files themselves.

If we’re starting from two different mental models (chatbot vs agent working in the repo), then it’s normal that this thread feels confusing.

About the setup: there’s no single right way. If I find the time I might record a short video showing mine, but don’t keep your expectations too high. :smile:

3 Likes

Guys, I’m planning on giving a walkthrough of my setup at the next Anvil User Group Meeting organised by Hassan Mamdani (not sure how to tag him ^^).

I think I have a decent setup where indeed as @stefano.menci mentioned I have the AI directly write code and read by notes on how to do it.

I bet there are many others out there who have similar or very different AI powered setups and I think all of us would benefit greatly from learning from each others’ best-practice approaches.

Ideally it coallesces to a standard AI-coding setup (an MCP server with best-practices for example) or something that everyone working with Anvil can use.

Anyways, happy to share what I’ve got going on, probably on April 29th :slight_smile:

Also @stefano.menci preparing the doc is super easy. I asked the AI to prepare it once, skimmed it and told it which sections I didnt like and what I wanted it to change. Then at the beginning of every task i tell the AI to read that doc. I then fix whatever i didnt like in the AI’s code and at the end of the task I tell the ai to fix the doc with the new learnings. I dont even check what it changes.

After I’ve completely revamped my frontend (should be done in a week) I will have the ai read my whole frontend codebase and make changes to the doc again. Maybe then ill read it ^^

1 Like

It sounds like you’re mostly focused on a single app. My situation is very different. I have hundreds of git repos: around 60 Anvil apps, 80 Excel VBA macros, plus Flask apps, CherryPy apps, CAD plugins, CAM plugins, IBM Notes repos, wiki repos, documentation, etc. So my problem isn’t “how do I structure one agents.md”, it’s how do I manage this across everything.

I had two big global versions with dozens of symlinks. It kind of worked, but I kept hitting friction because I often need rules that are specific to the current repo. So I’m restructuring it again.

We have an internal company wiki, and I create a wiki page for every tool. Inside each git repo I add a *.wiki text file with the content of that wiki page. At the end of every OpenCode session, my last prompt is always:

“update the wiki page to reflect the last changes”.

I also maintain a separate repo that contains the entire wiki — more than 1000 pages. That repo has its own OpenCode project. I use it to keep pages consistent with each other and avoid stale or contradictory documentation. Larger tools span multiple wiki pages, with links to any relevant page in the wiki when appropriate.

What I’m working on now is a modular agents.md setup, something like:

  • agents_wiki.md → explains that ..\wiki contains the company wiki. When asked to update wiki pages for this app, make sure the *.wiki files in this repo match the corresponding files in the main wiki repo, and include links to any relevant wiki pages when appropriate.
  • agents_anvil_ui.md → only UI rules (similar to yours, but strictly scoped).
  • agents_anvil_user_management.md → for apps that use authentication.
  • agents_excel_vba.md → VBA guidelines.
  • agents_excel_lib.md → internal Excel library rules.
  • etc.

Then the top-level agents.md — the only one automatically read — just includes (or excludes) the relevant sub-agent files, plus any repo-specific instructions.