Search by

asconsulting / contao-zyppy-classes

asconsulting

A module for the Contao CMS to allow for easy selection of pre-configured CSS classes.

Package info

github.com/asconsulting/contao-zyppy-classes

Homepage

Type:contao-bundle

pkg:composer/asconsulting/contao-zyppy-classes

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

5.0.2 2026-10-15 18:24 UTC

This package is auto-updated.

Last update: 2026-10-15 18:24:57 UTC


README

A Contao 5 bundle that replaces the free-text "CSS class" box with a controlled vocabulary. The site owner defines the class names once in Design > CSS classes; editors pick from dropdowns and checkbox lists instead of typing.

Pages, articles, content elements, front end modules, forms and form fields each gain a Class Settings legend with up to three pickers: one exclusive class, any number of common classes for that kind of element, and any number of site-wide global classes. The chosen classes reach the markup on every rendering path Contao has, without re-rendering anything.

Requirements

  • PHP 8.1 or newer, the floor Contao 5.3 itself declares. A newer Contao raises the PHP requirement with it.
  • Contao 5.3 or newer, with no upper bound. The Zyppy Suite supports the LTS releases and is tested against the current non-LTS release as well: the test suite passes on core-bundle 5.3.0, 5.7.13 and 6.0.2 (last run 2026-10-07).

Installation

With Contao Manager

The package is on Packagist, so no Composer repository has to be configured.

  1. Open Contao Manager (/contao-manager.phar.php on your site) and log in.
  2. On the Packages tab, type contao-zyppy-classes into the search field and click Add on asconsulting/contao-zyppy-classes in the results.
  3. Click Apply changes. The Manager runs Composer and installs the package and its dependencies.
  4. When Composer finishes, the Manager checks for pending database changes and offers to run the migration. Confirm it. If the prompt does not appear, open Maintenance and run Database migration there.

With Composer on the command line

composer require asconsulting/contao-zyppy-classes:^5.0
vendor/bin/contao-console contao:migrate

What the migration does

It creates the tl_zyppy_class table and adds three columns to each of the six element tables.

Usage

Defining the vocabulary

Open Design > CSS classes. One row of tl_zyppy_class is one selectable class. A row carries a scope, a type, a token and a label, plus a sorting number:

Column Values
scope global, or one of page, article, content, module, form, formField
type exclusive (the editor picks at most one) or common (any number)
token the CSS class that ends up in the markup
label what the editor sees; falls back to the token
sorting order within a scope and type; lower first

The global scope holds the one common list every element offers. There is no global exclusive list - a row saved that way is simply never read.

Making a picker mandatory

Each of the 13 vocabularies has a companion *Required checkbox in Settings that makes the matching picker mandatory (articleClassesRequired and so on). Ticking it also drops the blank "-" choice from the exclusive picker. A switch is ignored while its vocabulary is empty: "required, but nothing to choose" would make the record unsavable with no visible cause.

What editors see

tl_article, tl_content, tl_form, tl_form_field, tl_module and tl_page each gain three fields — exclusiveClass, commonClasses and globalCommonClasses — grouped under a Class Settings legend placed before the expert legend.

A vocabulary left empty takes its picker out of the palette entirely - label, field and, once the last of the three has gone, the class legend as well. It is also kept out of the editAll and overrideAll field choosers, which build their lists from the table's fields rather than from the palette.

Where the classes end up

Element Written to
page the <body> class
article, content element, front end module the element's own class attribute
form inserted with {{insert_form::x}} the form wrapper's class
form field the widget's class

The three fields are merged into one ordered, de-duplicated list and added alongside whatever the editor typed into the element's own CSS ID/class box; a class that is already there is not added twice.

Migration from legacy version

This package replaces the legacy package asconsulting/zyppy_class (namespace ZyppyClass\). The old name does not resolve any more.

The four legacy Zyppy packages — page, classes, popup and search — have to be upgraded together, in one composer update: old and new packages register the same DCA fields and module types, so a site that briefly has both installed double-registers. The walkthrough for the whole suite, including the order of operations and a verification checklist, is UPGRADING.md in the contao-zyppy-page repository. The part specific to this package:

  • Element data is byte-identical; there is no data migration. The legacy package declared exclusiveClass, commonClasses and globalCommonClasses on the same six tables with the same SQL. Nothing on the element side moves and nothing has to be re-picked.
  • What moves is the vocabulary. Until 2026-09-04 the 13 vocabularies were keyValueWizard fields on tl_settings, which is a DC_File container: saving wrote system/config/localconfig.php, a filesystem write that fails on a read-only deployment. ImportSettingsVocabularyMigration copies them into tl_zyppy_class on the first contao:migrate after upgrading, and never again - once the table holds a single row it stands down, so a class an editor deletes stays deleted. Only the vocabularies moved; the *Required switches are still tl_settings keys.
  • Migrate first, touch nothing first. localconfig.php is the migration's only data source. Hand-cleaning the old keys out of it before migrating, or adding even one class in the new back end module before migrating, loses the vocabulary for good. After migrating, the old keys are inert and can be removed at leisure.
  • Verify the result, not the exit code. Check that tl_zyppy_class holds roughly as many rows as you had tokens, and that all six scopes still show their pickers under Class Settings.
  • Templates: none, at either version. Nothing to audit.
  • Records holding the literal 0 token in commonClasses — a leftover of the old placeholder behaviour — render as class="0 …". Worth a look at real commonClasses values before and after.

Development

The source is at https://github.com/asconsulting/contao-zyppy-classes.

composer install
vendor/bin/phpunit --no-coverage

How the classes reach the markup

src/Resolver/ClassResolver.php merges an element's three fields into one ordered, de-duplicated token list. Four listeners deliver that list, and which one applies depends on whether the scope has an isVisibleElement call site in Contao\Controller:

Scope Listener Hook Target
article, content element, front end module ApplyClassesListener isVisibleElement $model->classes
{{insert_form::x}} ApplyFormClassesListener parseTemplate the form wrapper's class
page GeneratePageListener generatePage $objPage->cssClass (the <body> class)
form field CompileFormFieldsListener compileFormFields the widget's class

ApplyClassesListener is the important one. isVisibleElement fires before the element is constructed, and $model->classes is the one seam Contao honours on every rendering path — legacy Module and ContentElement subclasses, Hybrid, and both flavours of fragment reference. That is what lets the bundle work without re-rendering anything.

It also writes only when there is something to write: Model::__set() marks a model modified even when set to an empty array, and a modified model is inlined into its fragment reference instead of being passed by id.

Earlier versions applied the classes from getContentElement, getFrontendModule and getForm by re-rendering the element after mutating it. That rendered a ce_module four times and, for a form, re-ran processFormData() on each pass — duplicate rows, duplicate notification e-mails, duplicate uploads. Those listeners are gone. Do not reintroduce a render-time hook here.

Licence

Licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only). See LICENSE. Copyright Andrew Stevens.