Field Label Visibility allows site builders to customize the label of individual fields per widget directly from "Manage Form Display" for selected content types. Per-field options include:
- Hide the label accessibly
- Render the label with a custom text (singular/plural mode), tag, CSS class, alignment and bold styling
- A CKEditor-style toolbar UI for alignment and bold settings
The module is XSS-safe by design: tag allowlist, regex-filtered class names, alignment allowlist, and escaped text. Server-side validation mirrors the runtime sanitization.
Differences with similar projects
Unlike field_label_replacement and similar modules, this module:
- Operates at the form widget level (not the entity display level)
- Provides a CKEditor-style inline toolbar in Manage Form Display
- Supports semantic vs decorative tag distinction with automatic `aria-hidden` for decorative cases while preserving the original accessible name
Comments
Comment #2
vishal.kadamComment #3
avpadernoThank you for applying!
Before giving links helpful to understand how the review process works, what to expect from a review, and what to do to avoid a review takes more time than needed, I would like to thank all the reviewers for the work they do.
These applications are volunters-driven, which also means it is not possible to predict when an application will be marked fixed and the applicant will get the permission to opt projects into security advisory policy. While we aim to make an application as quick as possible, it is also important for us that more people review the project used for an application. In this way, we make sure applications do not miss some important points that should be instead reported.
Applications are not meant to be complete debugging sessions that eliminate every existing bug, though. I apologize if sometimes applications seem to go into too-detailed reviews.
Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review.
The important notes are the following.
Keep in mind that once the project is opted into security advisory coverage, only Security Team members may change coverage.
To the reviewers
Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.
The important notes are the following.
For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.
Comment #4
vishal.kadam1. Add a README.md file
The project is missing a README.md file, it should follow the template suggested in README.md template.
2. FILE: field_label_visibility.libraries.yml
version: VERSIONVERSION is only used by Drupal core modules. Contributed modules should use a literal string that does not change with the Drupal core version a site is using.
3. FILE: field_label_visibility.module
Since the module is declared compatible with Drupal 10.3, removing the function implementing the hook is not possible. The function still needs to be defined, but it calls the method defined by the service class, as described in Support for object oriented hook implementations using autowired services (Backwards-compatible Hook implementation for Drupal versions from 10.1 to 11.0).
4. FILE: src/Form/SettingsForm.php
With Drupal 10 and Drupal 11, there is no longer need to use #default_value for each form element, when the parent class is ConfigFormBase: It is sufficient to use #config_target, as in the following code.
Using that code, it is no longer needed to save the configuration values in the form submission handler: The parent class will take care of that.
Comment #5
pedroromán commentedworking...
Comment #6
pedroromán commentedThanks for the review. I've addressed the four points in tag 1.0.2 (commit 73ca5fa on 1.0.x):
Added README.md following the suggested template.
Replaced version: VERSION with the literal 1.0.x in field_label_visibility.libraries.yml.
field_label_visibility.module now exposes procedural hook entry points that delegate to the FieldLabelVisibilityHooks service, keeping the #[Hook] attributes for Drupal 11 while staying compatible with Drupal 10.3.
SettingsForm now uses #config_target with \Drupal\Core\Form\ConfigTarget, so the manual #default_value, validateForm and submitForm are gone. The toConfig closure preserves the intersect+sort defense-in-depth.
Comment #7
vishal.kadamComment #8
vishal.kadamComment #9
vishal.kadamIt is better not to create new releases during these applications, since a review could ask for a change that is not backward compatible with the existing releases. Just using a development version avoids those BC issues.
Comment #10
vishal.kadamFILE: field_label_visibility.module
Procedural hooks should be marked with the
#[LegacyHook]attribute.Comment #11
pedroromán commentedThanks again for the detailed feedback.
Pushed to the
1.0.xbranch (no new tag, per your guidance):460fb90: tagged the three procedural hook bridges infield_label_visibility.modulewith#[LegacyHook]and added theDrupal\Core\Hook\Attribute\LegacyHookimport. The bridges keep delegating to the OOPFieldLabelVisibilityHooksservice, but they are now correctly identified as legacy bridges rather than independent handlers. Theusestatement is a static alias only, so on Drupal 10.3 (where the attribute class does not exist yet) the attribute is simply not reflected and the bridges keep working —core_version_requirement: ^10.3 || ^11is preserved.a8977b8: two preventive cleanups while reviewing the module:tags: { name: drupal.module_listener }entry fromfield_label_visibility.services.yml. That tag is not defined by Drupal core and was not doing anything; hook discovery relies onautoconfigure: trueand the#[Hook]attribute, which matches how core hook services are registered.ConfigFormlabels and descriptions, every$this->t()call in the OOP hook class (including the validator error messages). Existing kernel test summary assertions were updated accordingly.Local verification on Drupal 11.x (DDEV, PHP 8.4):
phpcs --standard=Drupal,DrupalPractice: 0 violations.phpstan analyse --level=1: 0 errors.Setting back to "Needs review".
Comment #12
vishal.kadamPriority should be change as per Issue priorities.
Comment #13
vishal.kadamRest seems fine to me.
Please wait for other reviewers and Project Moderator to take a look and if everything goes fine, you will get the role.
Comment #14
avpadernosrc/Hook/FieldLabelVisibilityHooks.php
Documentation comments for methods and functions need to also document parameters and return value, if there is any.
For a static method, it is sufficient to use
t()or create a\Drupal\Core\StringTranslation\TranslatableMarkupinstance, which what the documentation fort()suggests.Control statements are allowed to exceed the 80-character limit. If the checked conditions would be too much, Drupal coding standards suggests using local variables for the conditions, as in the following code.
(Keep in mind that code in issues is not shown as entered; there could be new lines where the code did not have any.)
There is no need to cast the value returned by
$this->t()to a string; in contexts where a string is required, PHP does that cast automatically.field_label_visibility.services.yml
Services can be autowired starting with Drupal 9.3.x. This means that it is no longer necessary to give a list of service arguments in the .services.yml file; they will be retrieved from the constructor definition.
Comment #15
avpadernoWhat reported here in Differences with similar projects needs to be added to the project page.
Comment #16
pedroromán commentedAll findings from comment #14 have been addressed on the `1.0.x` branch. No new tag has been created.
**Commit `ed5cb89`** — review feedback:
- `src/Hook/FieldLabelVisibilityHooks.php`
- Every method (constructor, hook implementations, static validators and private helpers) now carries a complete docblock with `@param` and `@return` entries.
- The three static `#element_validate` callbacks (`validateCustomLabel`, `validateLabelClass`, `validateLabelId`) now use the `t()` function instead of `\Drupal::translation()->translate()`, since they run in a static context where `StringTranslationTrait` is not available.
- Unnecessary `(string)` casts around `$this->t()` calls in `widgetSettingsSummaryAlter()` have been removed. The only remaining cast is on the toolbar `aria-label`, justified inline because `Html::escape()` declares a `string` parameter and `strict_types` forbids implicit `Stringable` coercion.
- Long inline conditions (the "Hide label" `#states` selector, `$has_customization`, the wrapper-tag and id/style branches in `applyCustomLabel`, and the `widget`/`field_definition` instanceof check in `widgetSettingsSummaryAlter`) have been extracted into named local variables to keep each line below 80 characters.
- `field_label_visibility.module` — the three procedural `#[LegacyHook]` bridges gained matching `@param` and `@return` docblocks.
- `field_label_visibility.services.yml` — switched to `autowire: true`; the explicit argument list is gone, since both `ConfigFactoryInterface` and `TranslationInterface` are aliased in `core.services.yml`. Container compilation and runtime resolution have been verified.
**Commit `db31c0b`** — additional refactor (scope outside review feedback, but worth flagging):
The runtime no longer injects a parallel decorative element next to the field label. Three operating modes:
- **Default** (no wrapper tag): id, class and inline style are applied to the native `` element through Drupal's form element preprocess pipeline (two new `#[Hook]` services, `preprocess_form_element` and `preprocess_form_element_label`, plus their `#[LegacyHook]` procedural bridges). The `for=` association with the input is preserved.
- **Wrapper tag** (`span` / `small` / `mark`): the `#title` is replaced with a `Markup` instance that wraps the text in the chosen phrasing-content tag with the configured id/class/style; the wrapping `` stays in place. Tags not allowed as `` children (`h1`–`h6`, `p`, `legend`) have been removed from the option list because they would produce HTML invalid per the WHATWG spec.
- **Hide**: `#title_display = 'none'` instead of `'invisible'`. Core stops rendering the `` entirely; the markup is absent from the DOM rather than visually hidden.
End-to-end verified by rendering a `/node/add/post` form on a real Drupal 11 site:
- Default mode → `Custom`
- Wrapper mode → `Custom`
- Hide mode → no `` element in the DOM
The README has been rewritten to describe the new modes accurately.
**Quality gates**:
- `phpcs --standard=Drupal,DrupalPractice` — 0 violations
- `phpstan analyse --level=1` — 0 errors
- PHPUnit kernel tests — 17/17 passing, 227 assertions
**Project page update**: the "Differences with similar projects" content has been added to the project page itself, as requested.
Comment #17
avpadernofield_label_visibility.module
The usual short description (Hook implementations for the Field Label Visibility module.) gives more information about the file content.
The rest of that documentation comment is not information a developer would need. It seems the type of comments an AI-assisted software would suggest. A maintainer could just need a @todo note that reminds that legacy hooks can be removed when Drupal 11 is the minimum required Drupal core version.
The use of AI-assisted software to write code needs to be disclosed in the project page.
Documentation comments for hooks just need the short description. A long description can be added to add details necessary when developing the module, for example a reminder of why a hook has been used instead of another one, or why the hook implementation is done that way. The description of the parameters and the return value is not necessary; it is something AI-assisted software tends to add.
field_label_visibility.info.yml
May you explain why the module would require PHP 8.3 or any higher version?
src/Form/SettingsForm.php
May you describe what that code does and why is it necessary?
src/Hook/FieldLabelVisibilityHooks.php
Why does the code check
preg_match()returns 1?Only one of those lines is usually necessary. May you describe the reason for using a custom validation handler?
It is correct that using
$this->t()is not possible in static methods, but what are the alternatives?(As a side note, the comment explaining that
$this->t()cannot be used in static methods is not necessary.)That regular expression is not correct to validate a CSS class name. Drupal core has a class method that can be used to verify a CSS class name is valid.
If some characters are not allowed in a custom label, the form element description should say they are not allowed.
Why does the validation handler allow other characters
Html::escape()would convert to HTML entities?stylesshould never be used to style HTML markup.That code should use
Xss::filter(), which also prevent cross-site-scripting vulnerabilities.That code only works for English; for other languages, that does not work. In Russian, for example the singular form is used for 1, 101, and other numbers for which the plural form would be used in English.
Comment #18
pedroromán commentedThank you for the detailed review. Commit 5f86c36 on branch 1.0.x
addresses every point raised in #17. Summary below, mapped 1-to-1
to the findings in your comment.
**field_label_visibility.module**
- Documentation style — the file docblock has been trimmed to a
single short description ("Procedural hook bridges for
field_label_visibility.") and the per-hook bridges keep only
the `Implements hook_XXX()` short description; `@param` and
`@return` blocks have been removed.
**field_label_visibility.info.yml**
- PHP requirement — `php: '>=8.3'` has been removed. The module
uses nothing newer than PHP 8.1 (constructor promotion +
readonly), so `core_version_requirement: ^10.3 || ^11` is the
only constraint that is needed.
**src/Form/SettingsForm.php**
- The `toConfig` closure now carries an inline comment explaining
that the `checkboxes` element submits every option (selected and
unselected), why the result is intersected with the live
node-type list (so options removed since the form was rendered
cannot leak into config) and why `sort()` is applied (stable
output, no spurious diffs on config export).
**src/Hook/FieldLabelVisibilityHooks.php**
- `preg_match() === 1` is gone. The three server-side
`#element_validate` callbacks (`validateCustomLabel`,
`validateLabelClass`, `validateLabelId`) have been dropped
entirely. Invalid input is filtered at render time; valid
output is guaranteed by Drupal core helpers.
- CSS classes — each whitespace-separated token is now passed
through `\Drupal\Component\Utility\Html::cleanCssIdentifier()`
so the rendered value is always a valid CSS identifier. The
double `preg_replace` pass has been removed.
- Custom label — the `<` / `>` blacklist validator is gone. The
form element description now states that the text is rendered
as plain text and HTML special characters are escaped on output;
`Html::escape()` handles the rest in the render path.
- Inline styles removed. Bold, italic, underline and alignment
are now expressed as utility CSS classes shipped by the module:
`.flv-label--bold`, `.flv-label--italic`, `.flv-label--underline`,
`.flv-label--align-left`, `.flv-label--align-center` and
`.flv-label--align-right`. A new library
`field_label_visibility/label` (css/label.css) carries them and
is attached to the widget render array on demand. The module
no longer injects any `style=""` attribute.
- Wrapper-mode HTML is built with `\Drupal\Core\Template\Attribute`
for safe attribute serialization and passed through
`\Drupal\Component\Utility\Xss::filter()` with the
`['span', 'small', 'mark']` allowlist as defense in depth before
being wrapped in `Markup::create()`.
- Singular/Plural toggle removed. The previous
`explode('|', $custom)` approach only models the English
singular/plural binary; it does not represent the plural forms
of Russian, Arabic, Polish, Gaelic and others. The setting, its
schema entry, its summary line and the related Kernel tests
have been removed. Multilingual sites should use Drupal's
standard field-label translation flow.
**README.md**
- The "PHP 8.3 or newer" requirement line has been removed; the
Singular/Plural usage paragraph has been removed; the
sanitization paragraph now reflects `Html::cleanCssIdentifier()`
and the absence of inline styles.
**Quality**
- `phpcs --standard=Drupal,DrupalPractice --extensions=php,module,install,inc`
on the full module: 0 violations.
- PHP 8.3 syntax check on every PHP file: clean.
- No new release tag has been created during the review.
The "Differences with similar projects" content is already on the
project page (added in commit ed5cb89, comment #16).
Comment #19
pedroromán commentedComment #20
avpadernoFor four of the points I listed in my previous comment, I asked questions. Those points were not suggesting changes but expecting answers.
May you describe what that code does and why is it necessary?
Why does the code check
preg_match()returns 1?Only one of those lines is usually necessary. May you describe the reason for using a custom validation handler?
It is correct that using
$this->t()is not possible in static methods, but what are the alternatives?Comment #21
avpadernoAs a further note, these applications never require removing implemented features, as long as those features are not already provided by Drupal core and the code just need to be adjusted to use classes or functions made available by Drupal core.
Changing the code that was incorrectly building markup is correct, but removing code to avoid to correctly implementing a feature is not correct.
Comment #22
pedroromán commentedThanks for the clarifications.
A note on the features removed in earlier rounds: those were optional extras
I had added as small refinements, not core functionality. When the
application was set to Needs work I removed them so they would not hold up the
review, not to avoid implementing them properly. With your clarification in
#21 -- that the correct path is to adjust the code to use the classes Drupal
core provides rather than drop the feature -- the Singular/Plural option has
been restored and re-implemented correctly (see the last section). The four
questions are answered below.
--- 1. The toConfig closure in SettingsForm ---
The "bundles" element is #type => checkboxes. On submit a checkboxes element
returns an associative array keyed by *every* option (checked and unchecked),
checked ones truthy and unchecked ones 0. Writing that straight to config is
wrong on three counts: it would persist unchecked bundles as 0 values (the
schema declares a sequence of enabled machine names), the order would follow
the options array rather than be stable, and a node type deleted between form
build and submit could still leak in.
The toConfig callback of ConfigTarget (the #config_target API, Drupal 10.2+)
maps the submitted form value to the stored config value:
- array_filter() drops the unchecked (falsy) entries;
- array_keys() keeps only the checked bundle machine names;
- array_intersect($valid_bundles, ...) keeps only names that still match an
existing node type (defends against a type removed after the form was built);
- array_values() reindexes to a clean list;
- sort() makes the output deterministic, so the exported
field_label_visibility.settings.yml is stable across environments and does
not produce noise diffs on config export.
So the closure exists to convert the checkboxes submitted shape into the
clean, validated, deterministic sequence the config schema declares. It is the
intended use of ConfigTarget::toConfig.
--- 2. Why preg_match() is compared to 1 ---
preg_match() returns int|false: 1 if the pattern matches, 0 if it does not,
and FALSE on an internal error (e.g. a malformed pattern). Comparing strictly
against 1 (and, for the negative case, !== 1) is the correct, type-safe way to
branch on "matched", because it treats both "no match" (0) and "error" (FALSE)
as "not a valid match", which is the safe default and avoids the classic
truthiness pitfall around the FALSE return.
The specific blocking validator that rejected '<' / '>' was redundant (the
label text is escaped with Html::escape() on output) and was removed in the
round-4 refactor. The only remaining preg_match() call is in sanitizeId();
this push aligns it to the explicit `=== 1` comparison for consistency with
this contract.
--- 3. #pattern plus a custom #element_validate handler ---
You are right that only one is usually necessary. #pattern is the HTML5
client-side pattern attribute: advisory only, trivially bypassed, never a
security or correctness boundary. #element_validate is the authoritative
server-side check. Having both was redundant; if a validator is warranted the
server-side #element_validate is the one to keep, with #pattern only as an
optional progressive-enhancement hint.
In this module neither is needed: the CSS classes are normalized at render
time with Html::cleanCssIdentifier(), the id is filtered through an allowlist
regex in sanitizeId(), and the label text is escaped with Html::escape(). So
both the #pattern attributes and the custom #element_validate callbacks were
removed and the values are sanitized at render time instead of blocking the
form.
--- 4. Alternatives to $this->t() in static methods ---
In a static method $this is unavailable, so the options are:
- the procedural t() function, which the Drupal coding standards explicitly
allow in static/procedural context such as a static #element_validate
callback (this is what the removed callback used);
- instantiating \Drupal\Core\StringTranslation\TranslatableMarkup directly;
- \Drupal::translation()->translate() / ->formatPlural() via the container;
- best of all, avoiding the static context: implement the logic on an
instance using StringTranslationTrait (so $this->t() / $this->formatPlural()
work) or inject TranslationInterface.
The module currently has no static translation call left (the static
validators were removed). The restored Singular/Plural resolution uses
$this->formatPlural() on the hook service instance, i.e. the last option.
--- Singular/Plural re-implemented (commit f3201a8) ---
The Singular/Plural toggle is restored. When enabled, the custom label is
written as "Singular|Plural" and the form to display is resolved by Drupal
core's PluralTranslatableMarkup through formatPlural(), so plural-form
selection follows the active language plural rules instead of the previous
English-only binary. The item count is taken from the field item list the
widget is bound to. Schema, settings-summary line and Kernel coverage
(single, zero, multi-value, default mode, toggle-off literal, summary) were
added accordingly.
Setting back to Needs review.