Working with Form Components

Last updated on
3 April 2026

The NYSDS form components incorporate a lot of logic internally such as managing the label, description, error message, and hard-codes the placement of these elements. In Drupal, however, these elements are often rendered separately and have their own separate templates and/or components. Because of this, form components are not as straightforward to work with.

The work required to successfully implement the NYSDS on webform components relies on the developer's expertise with twig debugging. Understanding which templates to alter and which variables to apply in the components is essential.

We will soon be releasing a submodule of this module which includes examples for each component in use. Developers should find it easy to copy templates from that example module into their themes in order to start using NYSDS components in their webforms with relative ease. After that, we are considering creating a separate module for automatically implementing the NYSDS on form components. 

Until then, here's some examples of the methodology you might take with your webform components.

Individual Fields

In this case, the component is the datepicker with a date element.

First, you need to edit the form-element.html.twig file for this specific field type. Here you will remove the parts of the code referencing the errors, description, and label. NYSDS form elements incorporate these components into a single web component. There is no need for a preprocess function to pass these variables; they are already available to the template that renders the element's {{ children }} object. You'll note I have kept any classes on the wrapper intact, but you can choose to do whatever you wish in this regard.

{%
  set classes = [
    'js-form-item',
    'form-item',
    'js-form-type-' ~ type|clean_class,
    'form-item-' ~ name|clean_class,
    'js-form-item-' ~ name|clean_class,
    title_display not in ['after', 'before'] ? 'form-no-label',
    disabled == 'disabled' ? 'form-disabled',
    errors ? 'form-item--error',
  ]
%}
<div{{ attributes.addClass(classes) }} style="margin-bottom: 100px;">
  {% if prefix is not empty %}
    <span class="field-prefix">{{ prefix }}</span>
  {% endif %}
  {{ children }}
  {% if suffix is not empty %}
    <span class="field-suffix">{{ suffix }}</span>
  {% endif %}
</div>

Next, edit the template which renders the element itself. In our case, it's the input.html.twig template for the date field. Here, you can harness data in the element[] array to get the data you need to render the NYSDS component. To view everything available to the template, turn on your twig debugging and {{ dump() }} the variables.

{# Determine if there are any errors and generate an error string digestable by the errorMessage prop #}
{% set elementErrors = element['#errors'] %}
{% if elementErrors %}
  {% set errorMessage = elementErrors|render|striptags|replace({"\r\n": " ", "\n": " ", "\r": " "}) %}
  {% set showError = true %}
{% endif %}
{# Render the element #}
{% include 'nys_ds:datepicker' with {
  id: element['#date'],
  name: element['#name'],
  label: element['#title'],
  description: element['#description'],
  required: element['#required'],
  disabled: element['#disabled'],
  errorMessage: errorMessage,
  showError: showError,
} %}
{{ children }}  

Fieldsets

Fieldsets provide Drupal a set of wrappers to deal with fields that are grouped together. Drupal does not natively create theme hook suggestions for fieldsets based on element type. We need to help it out by creating one in our docroot/themes/custom/your_theme/your_theme.theme file:


/**
  * Implements hook_theme_suggestions_HOOK_alter().
  */
  function your_theme_theme_suggestions_fieldset_alter(array &$suggestions, array $variables, $hook) {
    # In order to select specific fieldsets to alter by type, we need to add theme suggestions.
    if (isset($variables['element']['#type'])) {
      $type = str_replace("-", "_", $variables['element']['#type']);
      $suggestions[] = $hook . '__' . $type;
    }
  }

This allows us to have a custom fieldset (fieldset--radios.html.twig) which usually handles all the variables that the NYSDS wrapper components handle. In this case, our example will be with radio buttons.

Please note that the NYSDS generates its own <fieldset> elements when needed. Drupal's initial <fieldset> is still required by Drupal to assign form validation. As such, we need to edit the template remove any aria attributes and add role="presentation" to the element. This is generally safe because the NYSDS adds its own aria attributes to its fieldset.

{%
    set classes = [
      'js-form-item',
      'form-item',
      'js-form-wrapper',
      'form-wrapper',
    ]
  %}
  {% set cleaned_attributes = create_attribute() %}
  {% for name, value in attributes %}
    {% if not name starts with 'aria-' %}
      {% set cleaned_attributes = cleaned_attributes.setAttribute(name, value) %}
    {% endif %}
  {% endfor %}
  <fieldset{{ attributes.addClass(classes) }}>
    {% if prefix %}
      <span class="field-prefix">{{ prefix }}</span>
    {% endif %}
    
    {# Determine if there are any errors and generate an error string digestable by the errorMessage prop #}
    {% set elementErrors = element['#errors'] %}
    {% if elementErrors %}
      {% set errorMessage = elementErrors|render|striptags|replace({"\r\n": " ", "\n": " ", "\r": " "}) %}
      {% set showError = true %}
    {% endif %}

    {# Render the element #}
    {% include 'nys_ds:radiogroup' with {
      id: element['#id'],
      label: element['#title'],
      description: element['#description']['#markup'],
      required: element['#required'],
      disabled: element['#disabled'],
      showError: showError,
      errorMessage: errorMessage,
      options: children,
    } %}

    {% if suffix %}
      <span class="field-suffix">{{ suffix }}</span>
    {% endif %}
  </fieldset>

Then, the extra weird thing about fieldsets is that much of the time the next 1 to 3 depth templates are just creating wrappers for each grouping. All they're doing is adding more divs and passing the children render array. We want to remove everything except {{ children }} in these templates. In the case of the radio buttons, it's going to be form-element-webform-radio.html.twig and radios.html.twig. Again, the only text besides comments in those templates should be

{{ children }} 

Failure to remove the additional superfluous HTML elements from nested templates is likely to cause unwanted behavior in the rendering of the component.

For radios, the next important template at the deepest level will be input--radio.html.twig which actually iterates and renders radio button individually. Don't forget the value prop.

{# Render the element #}
{% include 'nys_ds:radiobutton' with {
  id: element['#id'],
  name: element['#name'],
  label: element['#title'],
  value: element['#return_value'],
  description: element['#description'],
  disabled: element['#disabled'],
} %}
{{ children }}

Using custom attributes to select components

Some components or component options may not be tied directly to a specific form item. In these cases, it might be necessary to use custom attributes. In the Webform module, you can set a custom attribute on any form element in the "Advanced" tab of the Drupal user interface. The attributes must be presented in YAML format.

For example, let's assume we want to use the nys-toggle component. This component is actually a textbox, but rendered specially for binary operations. We don't want all of our textboxes to render as toggles, so we need to select it as such somehow. We can do that by setting a special attribute on the element through the Drupal UI in the "Advanced" tab:

data-toggle: true 

On the template where we're defining our checkboxes, we can do this:

{% if element['#attributes']['data-toggle'] %}
  {% include 'nys_ds:toggle' with {
    id: element['#id'],
    name: element['#name'],
    label: element['#title'],
    checked: element['#checked'],
    description: element['#description'],
    disabled: element['#disabled'],
  } %}
{% else %}
  {% include 'nys_ds:checkbox' with {
    id: element['#id'],
    name: element['#name'],
    label: element['#title'],
    checked: element['#checked'],
    description: element['#description'],
    disabled: element['#disabled'],
  } %}
{% endif %}

Now, wherever you have set the `data-toggle` property in your checkbox elements, the toggle will render instead of a regular checkbox.

Alternatively, of course, you could always alter the functionality of the checkbox itself to add the option for selecting a type, or you could go out and find a submodule that will add toggle functionality to webforms and just override that module's template. With Drupal, there's always more than one way to do things.

Help improve this page

Page status: No known problems

You can: