This is a vocabulary, not a component zoo. Bootstrap supplies the components; Mark supplies the voice. For fields that voice is unusually quiet — three bindings, no new classes, no parallel implementation of anything Bootstrap already does.
scss/_forms.scss
Exactly three properties, and all three for the same reason: Bootstrap resolves them from a Sass variable at build time, and Mark swaps palette, theme and mode at run time by rewriting custom properties. A value compiled into the stylesheet cannot follow. That is a mechanism mismatch, not a disagreement about taste — which is why it is the entire list.
| State | Bootstrap | Mark | Why it had to move |
|---|---|---|---|
| Resting border | $input-border-color, compiled to #dfe3ea |
var(--bs-border-color) |
Bootstrap ships no dark-mode override for it, so a field kept a light hairline on a dark page. |
| Focus ring | rgba($primary, .15), baked at build time |
var(--ring) |
The ring stayed the build-time brand while every other focus surface followed the live palette. |
| Checked fill | $primary, baked at build time |
var(--mk-primary) |
Same staleness: a checked box did not move when the palette did. |
Everything else on this page — padding, radius, the check glyph, floating labels, validation copy, the disabled treatment — is Bootstrap's, tuned once as Sass variables where the engine can still reason about it. Before adding a rule to the form layer, check whether Bootstrap already emits it.
label / control / hint / feedback
The rhythm every editor screen repeats: a label, the control, an optional hint, and the feedback line that replaces the hint when validation speaks. Reserving the hint line keeps the form from reflowing as fields validate.
.form-control
Sizes track the button sizes: .form-control-sm,
the bare control, .form-control-lg. Focus any of them to see
the ring and the 45%-alpha primary border — both follow the live palette.
.form-select
Two truths, stated honestly. .form-select is the plain case,
and the browser owns the popup — no design system can
style the open list on every platform, so Mark does not pretend to. When
the menu itself has to carry the voice, that is the listbox pattern, and it
lives under UI rather than here, because it is built from a button and a
dropdown rather than from a field.
.form-check
One family, three appearances. The checked fill is
var(--mk-primary), so all three move together when the palette
does — that is the one binding worth seeing on this page.
.is-valid / .is-invalid
Bootstrap's, unmodified. The state colours are --bs-danger and
--bs-success, which deliberately do not re-tint per
palette: an error should read as an error in every theme rather than
arriving in the brand colour.
the UI boundary
The controls above are the ones Bootstrap has. The ones it does not — a multi-select, a token or chip input, a combobox that filters as you type — are common enough elsewhere that their absence is felt, and they are the honest argument for a section beyond this one.
They are filed under UI rather than Forms, because they are built from a button and a dropdown rather than from a field, and because a chip is not form-specific: filters, facets and tags on a card all want the same object. Building them as compositions of Bootstrap's dropdown — whose popper, keyboard handling and dismissal are never reimplemented — is what keeps this a vocabulary rather than a zoo.