diff --git a/modules/ui_patterns_legacy/src/ComponentConverter.php b/modules/ui_patterns_legacy/src/ComponentConverter.php
index 05107f506e7370124fa66d39226bfdb5ef2e9d31..eb63c7578dfc3159b47a0168694969ba5f989f87 100644
--- a/modules/ui_patterns_legacy/src/ComponentConverter.php
+++ b/modules/ui_patterns_legacy/src/ComponentConverter.php
@@ -69,6 +69,10 @@ class ComponentConverter {
         "type" => 'object',
         "properties" => $this->getPropsFromSettings($source['settings']),
       ];
+      $required_props = $this->getRequiredPropsFromSettings($source['settings']);
+      if (!empty($required_props)) {
+        $target['props']['required'] = $required_props;
+      }
     }
     $extractor = new StoryExtractor($this->componentPluginManager);
     $extractor->setExtension($this->extension);
@@ -164,6 +168,19 @@ class ComponentConverter {
     return $props;
   }
 
+  /**
+   * Get required props from UI Patterns 1.x settings.
+   */
+  private function getRequiredPropsFromSettings(array $settings): array {
+    $props = [];
+    foreach ($settings as $setting_id => $setting) {
+      if (\array_key_exists('required', $setting) && $setting["required"]) {
+        $props[] = $setting_id;
+      }
+    }
+    return $props;
+  }
+
   /**
    * A small helper to convert a property.
    */
diff --git a/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php b/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php
index fd9d60fdac3b83ca5c0abe507ccef7c131c93515..f8ac162aab2b9bef40180793e4360933f6d16e04 100644
--- a/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php
+++ b/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\ui_patterns_legacy\Drush\Commands;
 
 use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
@@ -7,46 +9,51 @@ use Drupal\ui_patterns_legacy\ComponentConverter;
 use Drupal\ui_patterns_legacy\ComponentDiscovery;
 use Drupal\ui_patterns_legacy\ComponentWriter;
 use Drush\Attributes as CLI;
+use Drush\Commands\AutowireTrait;
 use Drush\Commands\DrushCommands;
-use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use Symfony\Component\Finder\Finder;
 
 /**
  * Drush commands of UI Patterns Legacy module.
  */
 final class UiPatternsLegacyCommands extends DrushCommands {
+  use AutowireTrait;
 
   /**
    * {@inheritdoc}
    */
   public function __construct(
+    #[Autowire(service: 'ui_patterns_legacy.component_converter')]
     private readonly ComponentConverter $converter,
+    #[Autowire(service: 'ui_patterns_legacy.discovery')]
     private readonly ComponentDiscovery $discovery,
+    #[Autowire(service: 'ui_patterns_legacy.component_writer')]
     private readonly ComponentWriter $writer,
+    #[Autowire(service: 'plugin.manager.sdc')]
     private readonly CachedDiscoveryInterface $componentsManager,
   ) {
     parent::__construct();
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container): static {
-    return new static(
-      $container->get('ui_patterns_legacy.component_converter'),
-      $container->get('ui_patterns_legacy.discovery'),
-      $container->get('ui_patterns_legacy.component_writer'),
-      $container->get('plugin.manager.sdc')
-    );
-  }
-
   /**
    * Migrate components from UI Patterns 1.x to UI Patterns 2.x.
    */
   #[CLI\Command(name: 'ui-patterns:migrate', aliases: ['upm'])]
   #[CLI\Argument(name: 'extension', description: 'Module or theme machine name.')]
-  #[CLI\Usage(name: 'ui-patterns:migrate my_theme', description: 'Migrate components, replace dependencies and API calls.')]
+  #[CLI\Usage(name: 'ui-patterns:migrate my_theme', description: 'Convert components, replace dependencies and API calls.')]
   public function migrate(string $extension): void {
+    $this->convertComponents($extension);
+    $extension_path = $this->discovery->getExtensionPath($extension);
+    $this->replaceDeprecatedCalls($extension_path);
+    $this->changeInfoFile($extension, $extension_path);
+    $this->changeComposerFile($extension_path);
+  }
+
+  /**
+   * Convert components.
+   */
+  protected function convertComponents(string $extension): void {
     $legacy_definitions = $this->discovery->discover($extension);
     $extension_path = $this->discovery->getExtensionPath($extension);
     $this->converter->setExtension($extension);
@@ -72,13 +79,9 @@ final class UiPatternsLegacyCommands extends DrushCommands {
       }
       $errors = $this->converter->validate($component);
       foreach ($errors as $error) {
-        $this->io()->error($error);
+        $this->logger()->error($error);
       }
     }
-    $extension_path = $this->discovery->getExtensionPath($extension);
-    $this->replaceDeprecatedCalls($extension_path);
-    $this->changeInfoFile($extension, $extension_path);
-    $this->changeComposerFile($extension_path);
   }
 
   /**
@@ -90,6 +93,7 @@ final class UiPatternsLegacyCommands extends DrushCommands {
    */
   protected function replaceDeprecatedCalls(string $extension_path): void {
     $finder = new Finder();
+
     $patterns = ['*.php', '*.inc', '*.module', '*.theme'];
     $finder->files()->name($patterns)->in($extension_path);
     foreach ($finder as $file) {
diff --git a/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml b/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml
index dd33f279a4f4dfb56105a5b6f66a3cc1b07a82a7..6a8191089002b9d4f182cef3611ed75bcbf9703e 100644
--- a/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml
+++ b/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml
@@ -13,6 +13,7 @@ services:
     class: Drupal\ui_patterns_legacy\ComponentConverter
     arguments:
       - "@renderer"
+      - "@plugin.manager.sdc"
   ui_patterns_legacy.component_writer:
     class: Drupal\ui_patterns_legacy\ComponentWriter
   ui_patterns_legacy.component_element_alter:
diff --git a/src/ComponentPluginManager.php b/src/ComponentPluginManager.php
index 768b7cc2f21019ab034e701fcba2b91dc40c95f4..17d0577e7d5724870fb69a3f0bd1dff3b63c13a5 100644
--- a/src/ComponentPluginManager.php
+++ b/src/ComponentPluginManager.php
@@ -73,6 +73,7 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
     // Adding custom logic.
     $fallback_prop_type_id = $this->propTypePluginManager->getFallbackPluginId("");
     $definition = $this->alterSlots($definition);
+    $definition = $this->annotateSlots($definition);
     $definition = $this->annotateProps($definition, $fallback_prop_type_id);
     return $definition;
   }
@@ -91,6 +92,21 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
     return $definition;
   }
 
+  /**
+   * Annotate each slot in a component definition.
+   */
+  protected function annotateSlots(array $definition): array {
+    if (empty($definition['slots'])) {
+      return $definition;
+    }
+    $slot_prop_type = $this->propTypePluginManager->createInstance('slot', []);
+    foreach ($definition['slots'] as $slot_id => $slot) {
+      $slot['ui_patterns']['type_definition'] = $slot_prop_type;
+      $definition['slots'][$slot_id] = $slot;
+    }
+    return $definition;
+  }
+
   /**
    * Annotate each prop in a component definition.
    *
@@ -98,6 +114,12 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
    * We add a 'ui_patterns' object in each prop schema of the definition.
    */
   protected function annotateProps(array $definition, string $fallback_prop_type_id): array {
+    // In JSON schema, 'required' is out of the prop definition.
+    if (isset($definition['props']['required'])) {
+      foreach ($definition['props']['required'] as $prop_id) {
+        $definition['props']['properties'][$prop_id]['ui_patterns']['required'] = TRUE;
+      }
+    }
     if (!isset($definition['props']['properties'])) {
       return $definition;
     }
@@ -107,13 +129,6 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
     foreach ($definition['props']['properties'] as $prop_id => $prop) {
       $definition['props']['properties'][$prop_id] = $this->annotateProp($prop_id, $prop, $fallback_prop_type_id);
     }
-    $slot_prop_type = $this->propTypePluginManager->createInstance('slot', []);
-    if (!empty($definition['slots'])) {
-      foreach ($definition['slots'] as $slot_id => $slot) {
-        $slot['ui_patterns']['type_definition'] = $slot_prop_type;
-        $definition['slots'][$slot_id] = $slot;
-      }
-    }
     return $definition;
   }
 
diff --git a/src/Element/ComponentPropForm.php b/src/Element/ComponentPropForm.php
index 767510be832329d09760e410a1341875863a11ff..e21838ff3c54f9af6f08793ea285a10ad6f998f8 100644
--- a/src/Element/ComponentPropForm.php
+++ b/src/Element/ComponentPropForm.php
@@ -93,6 +93,27 @@ class ComponentPropForm extends ComponentFormBase {
       'source' => $source_form,
     ];
     $element['#attributes']['style'] = 'position: relative;';
+    $element = static::addRequired($element, $prop_id);
+    return $element;
+  }
+
+  /**
+   * Add required visual clue to the fieldset.
+   *
+   * The proper required control is managed by SourcePluginBase::addRequired()
+   * so the visual clue is present whether or not the control is done by the
+   * source plugin. This is feature, not a bug.
+   */
+  protected static function addRequired(array $element, string $prop_id): array {
+    $component = static::getComponent($element);
+    if (!isset($component->metadata->schema["required"])) {
+      return $element;
+    }
+    $required_props = $component->metadata->schema["required"];
+    if (!in_array($prop_id, $required_props)) {
+      return $element;
+    }
+    $element["#required"] = TRUE;
     return $element;
   }
 
diff --git a/src/Plugin/UiPatterns/Source/AttributesWidget.php b/src/Plugin/UiPatterns/Source/AttributesWidget.php
index d78ad4aab2682231ea05ac4733862e161be1ec2f..6e0650d3bda16db709ac6099e6069038b92adb65 100644
--- a/src/Plugin/UiPatterns/Source/AttributesWidget.php
+++ b/src/Plugin/UiPatterns/Source/AttributesWidget.php
@@ -39,11 +39,11 @@ class AttributesWidget extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
+    $form = parent::settingsForm($form, $form_state);
     // Attributes are associative arrays, but this source plugin is storing
     // them as string in config.
     // It would be better to use something else than a textfield one day.
-    $build['value'] = [
+    $form['value'] = [
       '#type' => 'textfield',
       '#default_value' => $this->getSetting('value'),
     ];
@@ -52,9 +52,10 @@ class AttributesWidget extends SourcePluginBase {
     // See https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0
     $forbidden_characters = "<>&/`";
     $pattern = "^(([^" . $forbidden_characters . "]+)=[\"'].+[\"']\s*)*?$";
-    $build['value']['#pattern'] = $pattern;
-    $build['value']['#description'] = $this->t("Values must be present and quoted.");
-    return $build;
+    $form['value']['#pattern'] = $pattern;
+    $form['value']['#description'] = $this->t("Values must be present and quoted.");
+    $this->addRequired($form['value']);
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/CheckboxWidget.php b/src/Plugin/UiPatterns/Source/CheckboxWidget.php
index 42f6bceb40fe5015d6f55a1d16f4f568d488ae8f..84fe1e472bc47c00510478f5ec2c364d0829fc83 100644
--- a/src/Plugin/UiPatterns/Source/CheckboxWidget.php
+++ b/src/Plugin/UiPatterns/Source/CheckboxWidget.php
@@ -32,12 +32,12 @@ class CheckboxWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'checkbox',
-        '#default_value' => $this->getSetting('value'),
-      ],
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'checkbox',
+      '#default_value' => $this->getSetting('value'),
     ];
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/CheckboxesWidget.php b/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
index 6fb930be543dae407fc6c62f55d80f98d1d9e85e..8c5527466d5cb94effd8684fda25ddd9c5b43169 100644
--- a/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
+++ b/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
@@ -34,13 +34,14 @@ class CheckboxesWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'checkboxes',
-        '#default_value' => $this->getSetting('value') ?? [],
-        "#options" => $this->getOptions(),
-      ],
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'checkboxes',
+      '#default_value' => $this->getSetting('value') ?? [],
+      "#options" => $this->getOptions(),
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/EntityLinksSource.php b/src/Plugin/UiPatterns/Source/EntityLinksSource.php
index 242c3ceff73e216ac8d43ddb1bf120169fe0581b..c28dffeab70a6a576e5c52fb204ee45fcb39e4d8 100644
--- a/src/Plugin/UiPatterns/Source/EntityLinksSource.php
+++ b/src/Plugin/UiPatterns/Source/EntityLinksSource.php
@@ -109,12 +109,12 @@ class EntityLinksSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
+    $form = parent::settingsForm($form, $form_state);
     $entity_links = $this->getEntityLinkTemplates();
     $link_templates_options = array_keys($entity_links);
     $link_templates_options = array_combine($link_templates_options, $link_templates_options);
     asort($link_templates_options);
-    $build["template"] = [
+    $form["template"] = [
       '#type' => 'select',
       '#title' => $this->t("Select"),
       '#options' => $link_templates_options,
@@ -122,12 +122,12 @@ class EntityLinksSource extends SourcePluginBase {
       "#empty_option" => $this->t("- Select -"),
       '#empty_value' => '',
     ];
-    $build['absolute'] = [
+    $form['absolute'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Absolute URL'),
       '#default_value' => $this->isAbsoluteUrl(),
     ];
-    return $build;
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/ListTextareaWidget.php b/src/Plugin/UiPatterns/Source/ListTextareaWidget.php
index 84ac17dfa4f13618fe6365cbf1fa49abdb59620c..a18381ed655f992b2b976b8fa8428a4fda8efe30 100644
--- a/src/Plugin/UiPatterns/Source/ListTextareaWidget.php
+++ b/src/Plugin/UiPatterns/Source/ListTextareaWidget.php
@@ -32,17 +32,18 @@ class ListTextareaWidget extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
     $items = $this->getSetting('value');
     if (is_array($items)) {
       $items = implode("\r", $items);
     }
-    return [
-      'value' => [
-        '#type' => 'textarea',
-        '#default_value' => $items,
-        "#description" => $this->t("One item by line"),
-      ],
+    $form['value'] = [
+      '#type' => 'textarea',
+      '#default_value' => $items,
+      "#description" => $this->t("One item by line"),
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/MenuSource.php b/src/Plugin/UiPatterns/Source/MenuSource.php
index 3ddee49475588fd874c557be7dac32002fff3277..2dfd73e5bc76bf4c96268c7f5460fd4d3bb803f6 100644
--- a/src/Plugin/UiPatterns/Source/MenuSource.php
+++ b/src/Plugin/UiPatterns/Source/MenuSource.php
@@ -100,8 +100,8 @@ class MenuSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build["menu"] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form["menu"] = [
       '#type' => 'select',
       '#title' => $this->t("Menu"),
       '#options' => $this->getMenuList(),
@@ -109,14 +109,14 @@ class MenuSource extends SourcePluginBase {
     ];
     $options = range(0, $this->menuLinkTree->maxDepth());
     unset($options[0]);
-    $build['level'] = [
+    $form['level'] = [
       '#type' => 'select',
       '#title' => $this->t('Initial visibility level'),
       '#default_value' => $this->getSetting('level'),
       '#options' => $options,
     ];
     $options[0] = $this->t('Unlimited');
-    $build['depth'] = [
+    $form['depth'] = [
       '#type' => 'select',
       '#title' => $this->t('Number of levels to display'),
       '#default_value' => $this->getSetting('depth'),
@@ -125,7 +125,7 @@ class MenuSource extends SourcePluginBase {
         'This maximum number includes the initial level and the final display is dependant of the component template.'
       ),
     ];
-    return $build;
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/NumberWidget.php b/src/Plugin/UiPatterns/Source/NumberWidget.php
index 2d8ce1499adc6eb4fe94a6f515ed72f8735d9b95..cb81b78765bda8739745a30a8962ec4f1f1858fd 100644
--- a/src/Plugin/UiPatterns/Source/NumberWidget.php
+++ b/src/Plugin/UiPatterns/Source/NumberWidget.php
@@ -37,29 +37,30 @@ class NumberWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build['value'] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
       '#type' => 'number',
       '#default_value' => $this->getSetting('value'),
       '#step' => 0.01,
     ];
     if ($this->propDefinition["type"] === "integer") {
-      $build['value']['#step'] = 1;
+      $form['value']['#step'] = 1;
     }
     // Because of SDC's ComponentMetadata::parseSchemaInfo() which is adding
     // "object" type to all props to "allows deferring rendering in Twig to the
     // render pipeline". Remove it as soon as this weird mechanism is removed
     // from SDC.
     if (in_array("integer", $this->propDefinition["type"])) {
-      $build['value']['#step'] = 1;
+      $form['value']['#step'] = 1;
     }
     if (isset($this->propDefinition["minimum"])) {
-      $build['value']['#min'] = $this->propDefinition["minimum"];
+      $form['value']['#min'] = $this->propDefinition["minimum"];
     }
     if (isset($this->propDefinition["maximum"])) {
-      $build['value']['#max'] = $this->propDefinition["maximum"];
+      $form['value']['#max'] = $this->propDefinition["maximum"];
     }
-    return $build;
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/PathSource.php b/src/Plugin/UiPatterns/Source/PathSource.php
index e818f026b067d691bd1121d3572e693a5418a32c..9724cb52e976a3d38a0dd4b0eab9e1d14e14cfe2 100644
--- a/src/Plugin/UiPatterns/Source/PathSource.php
+++ b/src/Plugin/UiPatterns/Source/PathSource.php
@@ -48,15 +48,15 @@ class PathSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
     $value = $this->getSetting('value');
     $value = $this->getUrlFromRoute($value);
-    return [
-      'value' => [
-        '#type' => 'path',
-        '#default_value' => $value,
-        '#description' => $this->t("Enter an internal path"),
-      ],
+    $form['value'] = [
+      '#type' => 'path',
+      '#default_value' => $value,
+      '#description' => $this->t("Enter an internal path"),
     ];
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/SelectWidget.php b/src/Plugin/UiPatterns/Source/SelectWidget.php
index e80c6f1454db14b5ac0abce6f473c44ad1a566ff..5ab0bf3625a6e2dfc99ee315f1b2b58a682ad9dc 100644
--- a/src/Plugin/UiPatterns/Source/SelectWidget.php
+++ b/src/Plugin/UiPatterns/Source/SelectWidget.php
@@ -25,17 +25,18 @@ class SelectWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build['value'] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
       '#type' => 'select',
       '#default_value' => $this->getSetting('value'),
       "#options" => $this->getOptions(),
       "#empty_option" => $this->t("- Select -"),
     ];
+    $this->addRequired($form['value']);
     // With Firefox, autocomplete may override #default_value.
     // https://drupal.stackexchange.com/questions/257732/default-value-not-working-in-select-field
-    $build['value']['#attributes']['autocomplete'] = 'off';
-    return $build;
+    $form['value']['#attributes']['autocomplete'] = 'off';
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/TextfieldWidget.php b/src/Plugin/UiPatterns/Source/TextfieldWidget.php
index a20a9f36a9feac6a32106ca14c7208d1ad36a673..427e4810ddffd073de35273d1a8b929a2be58843 100644
--- a/src/Plugin/UiPatterns/Source/TextfieldWidget.php
+++ b/src/Plugin/UiPatterns/Source/TextfieldWidget.php
@@ -25,28 +25,28 @@ class TextfieldWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build['value'] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
       '#type' => 'textfield',
       '#default_value' => $this->getSetting('value'),
     ];
+    $this->addRequired($form['value']);
     $description = [];
     if (isset($this->propDefinition["pattern"])) {
-      $build['value']['#pattern'] = $this->propDefinition["pattern"];
+      $form['value']['#pattern'] = $this->propDefinition["pattern"];
       $description[] = $this->t("Constraint: @pattern", ["@pattern" => $this->propDefinition["pattern"]]);
     }
     if (isset($this->propDefinition["maxLength"])) {
-      $build['value']['#maxlength'] = $this->propDefinition["maxLength"];
-      $build['value']['#size'] = $this->propDefinition["maxLength"];
+      $form['value']['#maxlength'] = $this->propDefinition["maxLength"];
+      $form['value']['#size'] = $this->propDefinition["maxLength"];
       $description[] = $this->t("Max length: @length", ["@length" => $this->propDefinition["maxLength"]]);
     }
     if (!isset($this->propDefinition["pattern"]) && isset($this->propDefinition["minLength"])) {
-      // @todo Cover also the use case pattern + minLength.
-      $build['value']['#pattern'] = "^.{" . $this->propDefinition["minLength"] . ",}$";
+      $form['value']['#pattern'] = "^.{" . $this->propDefinition["minLength"] . ",}$";
       $description[] = $this->t("Min length: @length", ["@length" => $this->propDefinition["minLength"]]);
     }
-    $build['value']["#description"] = implode("; ", $description);
-    return $build;
+    $form['value']["#description"] = implode("; ", $description);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/TokenSource.php b/src/Plugin/UiPatterns/Source/TokenSource.php
index 25edb3ad006af2f2fb7f4b990214bd7905efaf43..46b6f180071b52325efb1ef843bd14590bb9fb31 100644
--- a/src/Plugin/UiPatterns/Source/TokenSource.php
+++ b/src/Plugin/UiPatterns/Source/TokenSource.php
@@ -68,15 +68,15 @@ class TokenSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'textfield',
-        '#default_value' => $this->getSetting('value'),
-        // Tokens always start with a [ and end with a ].
-        '#pattern' => '^\[.+\]$',
-      ],
-      'context_mapping' => $this->addContextAssignmentElement($this, $this->gatheredContexts),
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'textfield',
+      '#default_value' => $this->getSetting('value'),
+      // Tokens always start with a [ and end with a ].
+      '#pattern' => '^\[.+\]$',
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/UrlWidget.php b/src/Plugin/UiPatterns/Source/UrlWidget.php
index 8fdd58e306ab4dd0cac9e1b903f874eb084f1f6b..8bfefd6372ccb401b7b0d869497c5d50b5c4ec51 100644
--- a/src/Plugin/UiPatterns/Source/UrlWidget.php
+++ b/src/Plugin/UiPatterns/Source/UrlWidget.php
@@ -25,13 +25,14 @@ class UrlWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'url',
-        '#default_value' => $this->getSetting('value'),
-        '#description' => $this->t("Enter an external URL"),
-      ],
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'url',
+      '#default_value' => $this->getSetting('value'),
+      '#description' => $this->t("Enter an external URL"),
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/WysiwygWidget.php b/src/Plugin/UiPatterns/Source/WysiwygWidget.php
index 8bf60a7695f9c0af5acea2c9d3d4fa6893d8dffc..8e7978145b3adeaf483f5eb62e0107dbdd50f2f7 100644
--- a/src/Plugin/UiPatterns/Source/WysiwygWidget.php
+++ b/src/Plugin/UiPatterns/Source/WysiwygWidget.php
@@ -87,6 +87,7 @@ class WysiwygWidget extends SourcePluginBase implements TrustedCallbackInterface
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
     $value = $this->getSetting('value');
     $element = [
       '#type' => 'text_format',
@@ -101,7 +102,8 @@ class WysiwygWidget extends SourcePluginBase implements TrustedCallbackInterface
     else {
       $element['#format'] = filter_fallback_format();
     }
-    return ['value' => $element];
+    $form['value'] = $element;
+    return $form;
   }
 
   /**
diff --git a/src/PropTypePluginBase.php b/src/PropTypePluginBase.php
index 80279f4b1fd7a0aede1031fd04c05adc3c82a17b..aa4ffe51441fd1d47d41fd6da40fc2cce36bd661 100644
--- a/src/PropTypePluginBase.php
+++ b/src/PropTypePluginBase.php
@@ -56,6 +56,9 @@ abstract class PropTypePluginBase extends PluginBase implements PropTypeInterfac
     if (isset($definition['default']) && is_array($definition['default'])) {
       $summary[] = $this->t("Default: @default", ["@default" => implode(", ", $definition['default'])]);
     }
+    if (isset($definition['ui_patterns']['required']) && $definition['ui_patterns']['required']) {
+      $summary[] = $this->t("Required");
+    }
     return $summary;
   }
 
diff --git a/src/SourcePluginBase.php b/src/SourcePluginBase.php
index cde9fb796f56ff4978f0b1ba960670b8683f3f44..19c74ca9582533e96f4225e7112351a4abf83357 100644
--- a/src/SourcePluginBase.php
+++ b/src/SourcePluginBase.php
@@ -142,7 +142,7 @@ abstract class SourcePluginBase extends PluginBase implements
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [];
+    return $form;
   }
 
   /**
@@ -401,4 +401,21 @@ abstract class SourcePluginBase extends PluginBase implements
     }
   }
 
+  /**
+   * Add required property.
+   *
+   * @param array $form_element
+   *   The form element.
+   *
+   * @see \Drupal\ui_patterns\Element\ComponentPropForm::addRequired()
+   */
+  protected function addRequired(array &$form_element): void {
+    if (isset($this->propDefinition["ui_patterns"]["required"])) {
+      $form_element['#required'] = TRUE;
+      // ComponentPropForm is carrying the visual clue
+      // We set here the custom error message.
+      $form_element['#required_error'] = $this->t('@name field is required.', ['@name' => (!empty($form_element['#title'])) ? $form_element['#title'] : $this->propDefinition["title"]]);
+    }
+  }
+
 }
diff --git a/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php b/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php
index 8b42eb6f11b73c19625f64821c27162394a6b0c0..7bee5c157de91b738680c9b695e5f5527111a6d9 100644
--- a/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php
+++ b/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php
@@ -24,15 +24,14 @@ final class Foo extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      "value" => [
-        '#type' => 'textfield',
-        '#attributes' => [
-          'placeholder' => $this->t('Test: FOO'),
-        ],
-        '#default_value' => $this->getSetting('value'),
+    $form["value"] = [
+      '#type' => 'textfield',
+      '#attributes' => [
+        'placeholder' => $this->t('Test: FOO'),
       ],
+      '#default_value' => $this->getSetting('value'),
     ];
+    return $form;
   }
 
   /**