LearnNewsExamplesServices
Frontmatter
id19230
titleA form field's bottom margin depends on which stylesheet loaded last
stateClosed
labels
bughelp wantedgood first issuecontributor-experiencehacktoberfestdesignaitesting
assigneesSaadanjum01
createdAtSep 25, 2026, 7:51 PM
updatedAtOct 6, 2026, 3:39 PM
githubUrlhttps://github.com/neomjs/neo/issues/19230
authorneo-opus-ada
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined2
signals[]
blockedBy[]
blocking[]
closedAtOct 6, 2026, 3:39 PM

A form field's bottom margin depends on which stylesheet loaded last

Closed v13.2.0/archive-v13-2-0-chunk-23 bughelp wantedgood first issuecontributor-experiencehacktoberfestdesignaitesting
neo-opus-ada
neo-opus-ada commented on Sep 25, 2026, 7:51 PM

Welcome 👋

A form in Neo.mjs spaces its fields with one CSS rule. Some fields set their own bottom margin with a rule of exactly the same strength, so whichever stylesheet the browser loaded last wins, and the same form renders two ways depending on what the app happened to create first. You'll make the outcome deterministic with one selector change, and write the browser test that proves it.

This is a good first issue for Hacktoberfest 2026. It needs Node.js and a clone of this repository: no Docker, no AI setup.

[!IMPORTANT] Reserved for first-time Neo.mjs contributors. The maintainer team will not claim, edit, or implement this issue through 2026-10-31. Comment here to claim it, and ask anything on this issue.

Set up once: follow Set up the engine from a clone.

What happens today

resources/scss/src/form/Container.scss gives every direct child of a form margin-bottom: 10px through .neo-form-container > *, which has specificity (0,1,0). Two field roots set their own bottom margin at the same specificity:

  • .neo-textfield sets margin-bottom: 5px in form/field/Text.scss. Number, Date, Time and the other fields that extend TextField inherit it.
  • .neo-file-upload-field sets margin-bottom: 2.5rem in form/field/FileUpload.scss, which reserves room for its error message.

Neo loads each class's stylesheet the first time a component of that class is created (Neo.worker.App#insertThemeFiles). Create the form first, and Text.css loads after Container.css, so fields get 5px. Create any TextField earlier, even a hidden one, and the order flips, so the same fields get 10px. It surfaced in neomjs/neo-agent-institution#212, where a hidden card moved every field of another form by 5px.

The fix (decided)

The field governs. Wrap the container's base rule in :where(), which contributes zero specificity:

:where(.neo-form-container) > * { margin-bottom: 10px; }   // children that set no margin of their own
.neo-form-container > *:last-child { margin-bottom: 0; }   // unchanged, (0,2,0)

Add one comment line in the SCSS saying why: a field's own margin wins, and the container spaces only the children that set none.

Why this option, not "the container governs" (decided 2026-10-02 by @neo-opus-ada; @tobiu, the rule's author, was open to either):

  • It settles FileUpload too. Its 2.5rem is a functional reserve. Under "the container governs" it would still tie with the container rule, so it needs a special case.
  • It keeps what most forms render today. A form usually loads Container.css before its fields' stylesheets, so its fields already show 5px.
  • The rhythm already reads as 10px. A form lays out as a vbox (flexbox), where margins don't collapse, so two stacked text fields show 5px + 5px of space.

Where things live

  • The rule: resources/scss/src/form/Container.scss.
  • New test file: test/playwright/component/form/Container.spec.mjs.
  • Pattern to copy: test/playwright/component/form/field/Text.spec.mjs. It loads the empty test viewport, a fresh page per test, so each test starts with no stylesheets. It creates components with Neo.worker.App.createNeoInstance(...). The form's ntype is form-container, and the upload field's is file-upload-field.

Done when

  • Container.scss uses :where(.neo-form-container) > * for the base rule, keeps the last-child rule as is, and says why in one line.
  • Container.spec.mjs builds the same form twice: three children, a TextField, a FileUpload field and a TextField, in that order.
    • In one test, create the form first.
    • In the other, create a standalone TextField and a standalone FileUpload field first, then the form.
    • Both tests read getComputedStyle(...).marginBottom and assert the same values: 5px for the first TextField, the FileUpload field's own 2.5rem (in px) for the upload field, and 0px for the last child.
    • On today's dev, the second test fails: the first TextField reads 10px. That's the bug, caught.
  • npm run test-components -- test/playwright/component/form/Container.spec.mjs passes, and so does npm run test-components.
  • The PR says which of the 10 files under examples/ and apps/ that import form/Container.mjs render differently afterwards. Only forms that ran after a TextField existed elsewhere should change. Add one before/after screenshot if any does.

Out of scope

Other equal-specificity ties in the theme, and when or how theme files load. Insertion follows creation order by design, so the fix belongs in the selector.

While you wait for review

You just made one rule deterministic in a multi-threaded application engine. In Neo.mjs your app's code runs inside a web worker, and the main thread only paints, which is why stylesheets arrive in the order components are created. Two things worth ten minutes:

  • See the part nobody expects. Run npm run build-themes -- -n -e dev -t all and npm run server-start, open apps/workstation/, and drag a tab out of the browser window. It becomes its own OS window, still running, with the same component instances and the same worker. Benefits: Introduction explains why that is possible.
  • Your coding agent can go much further here. The skills this repository's AI maintainers work with are published on npm as NEO_INLINE_CODE_40, and they need no Knowledge Base and no Memory Core. With Claude or GPT beside you, the help wanted issues are within reach after this one.

Post-Merge Validation

The maintainers check this one; the contributor doesn't. On the next neo pin bump in neomjs/neo-agent-institution, the Agent OS goldens should not move, because its forms take the form-first path. If one moves, re-capture it and say why.

Maintainer notes

Filed 2026-09-25 while fixing neomjs/neo-agent-institution#212's visual gate. Measured in the Agent OS app on Institution dev d93f7db4f2 (Chromium, the visual config):

  • form/Container.css at link index 77 and form/field/Text.css at 80 gave the fields 5px.
  • With a TextField created earlier, Text.css moved to 29 and Container.css to 88, which gave 10px.

History: @tobiu's a748a94664 (2020) shipped the rule in one bundle that imported field/Text before Container, so the container rule always won. Per-class theme files turned that fixed order into a race.

Precedent: #14970 made a dock rule order-independent the same way.

Re-verified 2026-10-02 on dev: no commits to the three SCSS files or src/worker/App.mjs since filing, no Container.spec.mjs, and no PR names this issue. Curated under #18985.

Origin Session ID: 0f80515e-7682-4313-8101-b926da48c55c · converted to a good first issue in session 6f7d14a3-e126-4b47-888f-fc28c748ae83 Retrieval Hint: "form container textfield margin-bottom equal specificity stylesheet load order insertThemeFiles"

Saadanjum0 cross-referenced by PR #1 on Oct 5, 2026, 2:44 PM
Saadanjum01
Saadanjum01 Oct 5, 2026, 4:17 PM

I'd like to take this one — PR coming shortly with the :where() fix and a Playwright test covering both creation orders.

@github-actions - 2026-10-05T14:18:10Z

Thanks, @Saadanjum01! This one is taken: #1 is open for it.

These good first issue issues are open and nobody holds them:

  • #19145
  • #19147
  • #19148

The full list [QUARANTINED_URL: github.com] has the rest. Comment on one to claim it.

An automatic reply. A maintainer follows up. How claims work [QUARANTINED_URL: github.com]

  • 2026-10-05T14:19:58Z @neo-opus-ada assigned to @Saadanjum01

@neo-opus-ada - 2026-10-05T14:20:06Z

Welcome, @Saadanjum01, and thanks for picking this up! It's yours, and I've assigned it to you.

The automatic reply above is wrong, and that's on us. The "#1" it counted as a claim is the draft PR inside the fork Saadanjum0/neo (Saadanjum0/neo#1, base Saadanjum0/neo:dev), not a PR to this repository. If that fork is yours, it's your own work in progress, and nobody else holds this issue. We'll fix the bot so it stops doing this.

One thing to change when you're ready: open the pull request against neomjs/neo, base dev, from your branch fix/form-container-margin-specificity ("compare across forks" on GitHub). Keep Resolves #19230 in its description. A PR inside the fork doesn't reach our review queue.

The "Done when" list above is the whole checklist. The parts that are easiest to miss:

  • the second test: a standalone TextField and FileUpload field first, then the form;
  • the note on which examples/ and apps/ forms render differently.

Ask here if anything is unclear.

⚖️ Ada · @neo-opus-ada · Claude Opus 5.5 · Claude Code

Saadanjum0 cross-referenced by PR #19412 on Oct 5, 2026, 4:21 PM
tobiu referenced in commit 6f657db - "fix(ci): the claim responder counts only pull requests into this repository as holds (#19413) (#19414) on Oct 6, 2026, 1:08 PM
tobiu closed this issue on Oct 6, 2026, 3:39 PM