xddesigners / silverstripe-better-forms
Nicer CMS forms: a Bootstrap grid layout field, help tooltips after labels, and per-field colour/outline styling.
Package info
github.com/xddesigners/silverstripe-better-forms
Language:JavaScript
Type:silverstripe-vendormodule
pkg:composer/xddesigners/silverstripe-better-forms
Requires
- php: ^8.3
- silverstripe/admin: ^3.0
- silverstripe/framework: ^6.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Nicer CMS forms with a small, chainable PHP API. Most features are just a little CSS + JS injected
into the admin — no dev/build needed — and the grid field is a normal form field.
- Grid layout — lay fields out in responsive columns with a
GridLayoutField, driving the Bootstrap 5 grid thatsilverstripe/adminalready ships. - Help tooltips — a small (i) icon after a label that reveals an explanation on hover/focus.
- Placeholders —
setPlaceholder()on any field (Silverstripe core only has it on searchable dropdowns). - Field styling — label colour & font, input text / background / border colour, inline option sets, and a full-width break-out.
- Description styling — colour, callout box and font style for a field's description.
Requirements
silverstripe/framework^6.0silverstripe/admin^3.0
Installation
composer require xddesigners/silverstripe-better-forms
1. Grid layout
GridLayoutField is a CompositeField whose children are placed in Bootstrap columns. Give it
fields and a column map, or add fields with their span inline.
use XD\BetterForms\Forms\GridLayoutField; use SilverStripe\Forms\TextField; $fields->addFieldToTab('Root.Main', GridLayoutField::create('NameRow', [ TextField::create('FirstName', 'First name'), TextField::create('LastName', 'Last name'), ])->setColumns([ 'FirstName' => 6, // col-md-6 'LastName' => 6, ]));
Spans may be a bare int (applied at the default breakpoint, md) or a per-breakpoint map:
GridLayoutField::create('Address') ->addColumn(TextField::create('Street'), ['md' => 8, 'lg' => 9]) ->addColumn(TextField::create('Number'), ['md' => 4, 'lg' => 3]);
By default a grid keeps the admin's readable width (like a normal field). Call enableFullWidth()
to break it out to the whole form row — handy for wide, column-heavy layouts:
GridLayoutField::create('WideRow', [ TextField::create('City'), TextField::create('Zip'), TextField::create('Country'), ])->setColumns(['City' => 4, 'Zip' => 4, 'Country' => 4]) ->enableFullWidth();
Helpers:
| Method | Does |
|---|---|
setColumns(['Field' => 6, ...]) |
Assign spans to existing children by name (int, or ['md'=>8]). |
addColumn($field, 6 | ['md'=>8]) |
Push a field with its span. |
setGutter('bf-gutter-sm') |
Column gap. Presets: bf-gutter-0 · bf-gutter-sm · bf-gutter-md (default) · bf-gutter-lg. A Bootstrap g-* class works too. |
setBreakpoint('md') |
Breakpoint used for bare-int spans (use xs for always-on columns). |
enableFullWidth() |
Span the whole form row instead of the readable-width column. |
Nest GridLayoutFields for more complex layouts. Columns stack on narrow screens, per Bootstrap.
Bonus: setColumnCount() works again
Silverstripe's native CompositeField::setColumnCount(n) lost its layout CSS in silverstripe/admin
3.x. This module re-ships it, so the zero-config equal-columns option works too:
CompositeField::create($fieldA, $fieldB, $fieldC)->setColumnCount(3);
2. Help tooltips
Add an (i) icon after any field's label:
TextField::create('VAT', 'VAT number') ->setTooltip('Include the country prefix, e.g. NL123456789B01');
Prefer to reuse the field's existing description as the tooltip (keeps the form uncluttered):
TextField::create('Slug') ->setDescription('Lowercase, no spaces — used in the URL.') ->convertDescriptionToTooltip();
…or do that for every field in the CMS, from YAML:
XD\BetterForms\BetterForms: descriptions_as_tooltips: true
The icon is a CMS font-icon (info-circled by default). Change it globally or per field:
XD\BetterForms\BetterForms: info_icon: 'help-circled'
$field->setInfoIcon('help-circled'); // another CMS font-icon $field->setInfoIcon('fa-solid fa-circle-info'); // or Font Awesome (see below)
3. Placeholders & autocomplete
Silverstripe has no setPlaceholder() on plain text fields — this adds one to every field. A
placeholder is an example value, not a label (it disappears once the user types), so keep the field's
real label too:
TextField::create('Name', 'Name')->setPlaceholder('e.g. Jane Doe'); EmailField::create('Email', 'Email')->setPlaceholder('name@example.com');
setAutocomplete() sets the field's HTML autocomplete token so browsers and password managers can
autofill it — which also satisfies WCAG 1.3.5 Identify Input Purpose:
EmailField::create('Email', 'Email')->setAutocomplete('email'); TextField::create('Phone', 'Phone')->setAutocomplete('tel'); TextField::create('Zip', 'Postcode')->setAutocomplete('postal-code');
4. Field styling
Chainable setters on any field. Label colour and font:
$field->setLabelColor('#c0392b') ->setLabelFontStyle('bold'); // 'bold' | 'italic' | 'bold italic' | 'normal'
Input text, background and border colour (text inputs, textareas and selects, incl. Chosen):
TextField::create('Price', 'Price') ->setFieldColor('#111') // input text colour ->setFieldBackground('#fffbea') // input background ->setFieldBorderColor('#c0392b'); // input border colour
Full width — break any field out of the admin's ~58% readable-width cap so its control spans the
whole form row (the label sits on its own line above). Great for an HTMLEditorField or GridField:
HTMLEditorField::create('Content')->enableFullWidth();
Inline option sets — lay an OptionsetField's or CheckboxSetField's options out in a
horizontal, wrapping row instead of stacked vertically:
OptionsetField::create('Size', 'Size', ['s' => 'S', 'm' => 'M', 'l' => 'L'])->enableInline();
5. Description styling
Colour the description text, set its font style, or turn it into a padded callout box:
// coloured, italic help text $field->setDescriptionColor('#c0392b') ->setDescriptionFontStyle('italic'); // 'bold' | 'italic' | 'bold italic' | 'normal' // a background and/or border turns the description into a padded callout box $field->setDescriptionBackground('#fffbea') ->setDescriptionBorderColor('#e0c84a'); // …or set it all in one call: setDescriptionStyle($color, $background, $borderColor, $fontStyle) // (any argument may be null) $field->setDescriptionStyle('#5a4a00', '#fffbea', '#e0c84a', 'bold');
Required fields & validation
Make fields required with a RequiredFieldsValidator on the record's CMS validator — the CMS then flags
them and blocks saving until they're filled. In Silverstripe CMS 6 both classes live under
SilverStripe\Forms\Validation\ (in CMS 5 it was SilverStripe\Forms\RequiredFields):
use SilverStripe\Forms\Validation\CompositeValidator; use SilverStripe\Forms\Validation\RequiredFieldsValidator; public function getCMSCompositeValidator(): CompositeValidator { $validator = parent::getCMSCompositeValidator(); $validator->addValidator(RequiredFieldsValidator::create([ 'Name', 'Email', ])); return $validator; }
Better Forms then makes the required state and the resulting errors accessible automatically (every form, no extra code):
- a visible required marker (a
*after the label, in the CMS's own danger colour) — the CMS setsrequired/aria-requiredbut shows nothing visually. Each tab that has a required field also gets a localised legend at the bottom ("Fields marked with an * are required"). Turn both off withrequired_markers: false. - on a failed save, each errored field is marked
aria-invalidand linked to its error message viaaria-describedby, so a screen reader conveys the error whenever the field is focused — and focus moves to the first field in error (WCAG 3.3.1, 3.3.3). Both are undone once the field validates.
XD\BetterForms\BetterForms: required_markers: true # the "*" marker; the error wiring above is always applied
Accessibility
Installing the module also improves the base accessibility of every CMS form, not just fields that use its API:
- Field descriptions are linked to their control. Silverstripe renders a field's description text but
does not associate it with the input, so screen readers don't announce it on focus. This module adds the
missing
aria-describedbyfor every field with a description — automatically, no code changes (1.3.1). A description moved into a tooltip (descriptions_as_tooltips/convertDescriptionToTooltip()) is announced via the tooltip instead. - Required fields and validation errors are made accessible — a visible required marker, plus
aria-invalid, error association and focus-to-first-error on a failed save. See Required fields & validation above.
Where you use the module's own API:
-
Tooltips are built to be accessible: the trigger is a real focusable
<button>, its text is exposed to screen readers viaaria-describedby, and the bubble is keyboard-operable — it shows a visible focus ring, stays open while hovered, and is dismissible with Esc (WCAG 1.4.13). That said, a tooltip is the right home only for a brief, non-essential hint. Anything a user needs to complete the field should stay a visible description — so reach fordescriptions_as_tooltips/convertDescriptionToTooltip()to declutter optional help, not to hide required instructions. -
Colours are yours, so is the contrast.
setLabelColor/setFieldColor/setFieldBackground/setFieldBorderColor/setDescription…apply exactly what you pass. Keep text at ≥ 4.5:1 and borders at ≥ 3:1 against their background (1.4.3 / 1.4.11), and never let colour alone carry meaning (e.g. a red border for “required” or “invalid”) — pair it with text or an icon (1.4.1). To catch low-contrast combinations while developing, turn on console warnings:XD\BetterForms\BetterForms: contrast_warnings: true # dev aid — logs a warning when a colour falls below WCAG AA
-
Placeholders are example values, not labels.
setPlaceholder()adds a nativeplaceholder, which vanishes once the user types and often renders below the contrast minimum — so keep a real label (Silverstripe renders one) and never use the placeholder as the only hint.
API reference
All setters return the field, so they chain.
GridLayoutField (extends CompositeField)
| Method | Does |
|---|---|
setColumns(array $map) |
Spans for existing children by name. |
addColumn(FormField $field, int|array $span) |
Push a field with its span. |
setGutter(string $gutter) |
Gutter preset (bf-gutter-0|sm|md|lg) or Bootstrap g-*. |
setBreakpoint(string $breakpoint) |
Default breakpoint for bare-int spans. |
enableFullWidth(bool $enabled = true) |
Span the whole form row. |
Every FormField (via extension)
| Method | Does |
|---|---|
setTooltip(string $text) |
Show an (i) tooltip after the label. |
convertDescriptionToTooltip(bool $enabled = true) |
Render the field's description as the tooltip. |
setInfoIcon(string $icon) |
Override the tooltip icon (CMS font-icon or fa-*). |
setPlaceholder(string $text) |
Set the input's native placeholder (an example value, not a label). |
setAutocomplete(string $token) |
Set the autocomplete token (e.g. email, tel) — WCAG 1.3.5. |
setLabelColor(string $color) |
Colour the label. |
setLabelFontStyle(string $style) |
Label font: bold / italic / bold italic / normal. |
setFieldColor(string $color) |
Input text colour. |
setFieldBackground(string $color) |
Input background colour. |
setFieldBorderColor(string $color) |
Input border colour. |
setDescriptionColor(string $color) |
Description text colour. |
setDescriptionBackground(string $color) |
Description background (→ callout box). |
setDescriptionBorderColor(string $color) |
Description border (→ callout box). |
setDescriptionFontStyle(string $style) |
Description font style. |
setDescriptionStyle(?$color, ?$background, ?$borderColor, ?$fontStyle) |
All description styling in one call. |
enableInline(bool $enabled = true) |
Horizontal options for Optionset/CheckboxSet. |
enableFullWidth(bool $enabled = true) |
Break the field out to the full form width. |
Full example
Every option exercised on one form — the demo page's Field types tab:
Font Awesome (optional)
Only needed if you pass fa-* classes to setInfoIcon().
XD\BetterForms\BetterForms: include_fontawesome_free: true # Font Awesome Free from cdnjs # fontawesome_css: 'https://kit.fontawesome.com/XXXX.css' # …or your own Pro kit
Theming (CSS variables)
Override the tooltip look in your admin CSS:
:root { --bf-tip-bg: #43536d; --bf-tip-color: #fff; --bf-tip-max-width: 260px; }
License
BSD-3-Clause.




