Search by

xddesigners / silverstripe-better-forms

RVXD

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

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.3.2 2026-10-09 14:26 UTC

This package is auto-updated.

Last update: 2026-10-09 14:26:29 UTC


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 that silverstripe/admin already 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.0
  • silverstripe/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.

Grid layout: responsive columns, per-breakpoint spans, full-width break-out and gutter presets

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);

Revived CompositeField::setColumnCount() — equal columns

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)

Help tooltips: an (i) icon after the label reveals help on hover/focus

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();

Per-field styling: label colour and font, input text/background/border colour, and description styling

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 sets required / aria-required but 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 with required_markers: false.
  • on a failed save, each errored field is marked aria-invalid and linked to its error message via aria-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-describedby for 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 via aria-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 for descriptions_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 native placeholder, 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:

Better Forms — every option on a single form

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.