diff --git a/core/core.services.yml b/core/core.services.yml
index 562d64f..b7202df 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -724,6 +724,9 @@ services:
   plugin.manager.condition:
     class: Drupal\Core\Condition\ConditionManager
     parent: default_plugin_manager
+  plugin.manager.element_info:
+    class: Drupal\Core\Render\ElementInfoManager
+    parent: default_plugin_manager
   kernel_destruct_subscriber:
     class: Drupal\Core\EventSubscriber\KernelDestructionSubscriber
     tags:
@@ -877,8 +880,7 @@ services:
   info_parser:
     class: Drupal\Core\Extension\InfoParser
   element_info:
-    class: Drupal\Core\Render\ElementInfo
-    arguments: ['@module_handler']
+    alias: plugin.manager.element_info
   file.mime_type.guesser:
     class: Drupal\Core\File\MimeType\MimeTypeGuesser
     tags:
diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc
index 7ab2059..55b4a43 100644
--- a/core/includes/ajax.inc
+++ b/core/includes/ajax.inc
@@ -5,6 +5,8 @@
  * Functions for use with Drupal's Ajax framework.
  */
 
+use Drupal\Core\Render\Element;
+
 /**
  * @defgroup ajax Ajax framework
  * @{
@@ -228,20 +230,10 @@
 /**
  * Form element processing handler for the #ajax form property.
  *
- * @param $element
- *   An associative array containing the properties of the element.
- *
- * @return
- *   The processed element.
- *
- * @see ajax_pre_render_element()
+ * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAjaxForm().
  */
-function ajax_process_form($element, &$form_state) {
-  $element = ajax_pre_render_element($element);
-  if (!empty($element['#ajax_processed'])) {
-    $form_state['cache'] = TRUE;
-  }
-  return $element;
+function ajax_process_form(&$element, &$form_state, &$complete_form) {
+  return Element\FormElement::processAjaxForm($element, $form_state, $complete_form);
 }
 
 /**
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 2e653ef..482234c 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2704,65 +2704,10 @@ function drupal_pre_render_html_tag($element) {
 /**
  * Pre-render callback: Renders a link into #markup.
  *
- * Doing so during pre_render gives modules a chance to alter the link parts.
- *
- * @param $elements
- *   A structured array whose keys form the arguments to l():
- *   - #title: The link text to pass as argument to l().
- *   - One of the following
- *     - #route_name and (optionally) and a #route_parameters array; The route
- *       name and route parameters which will be passed into the link generator.
- *     - #href: The system path or URL to pass as argument to l().
- *   - #options: (optional) An array of options to pass to l() or the link
- *     generator.
- *
- * @return
- *   The passed-in elements containing a rendered link in '#markup'.
+ * @deprecated Use \Drupal\Core\Render\Element\Link::preRenderLink().
  */
 function drupal_pre_render_link($element) {
-  // By default, link options to pass to l() are normally set in #options.
-  $element += array('#options' => array());
-  // However, within the scope of renderable elements, #attributes is a valid
-  // way to specify attributes, too. Take them into account, but do not override
-  // attributes from #options.
-  if (isset($element['#attributes'])) {
-    $element['#options'] += array('attributes' => array());
-    $element['#options']['attributes'] += $element['#attributes'];
-  }
-
-  // This #pre_render callback can be invoked from inside or outside of a Form
-  // API context, and depending on that, a HTML ID may be already set in
-  // different locations. #options should have precedence over Form API's #id.
-  // #attributes have been taken over into #options above already.
-  if (isset($element['#options']['attributes']['id'])) {
-    $element['#id'] = $element['#options']['attributes']['id'];
-  }
-  elseif (isset($element['#id'])) {
-    $element['#options']['attributes']['id'] = $element['#id'];
-  }
-
-  // Conditionally invoke ajax_pre_render_element(), if #ajax is set.
-  if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) {
-    // If no HTML ID was found above, automatically create one.
-    if (!isset($element['#id'])) {
-      $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link');
-    }
-    // If #ajax['path] was not specified, use the href as Ajax request URL.
-    if (!isset($element['#ajax']['path'])) {
-      $element['#ajax']['path'] = $element['#href'];
-      $element['#ajax']['options'] = $element['#options'];
-    }
-    $element = ajax_pre_render_element($element);
-  }
-
-  if (isset($element['#route_name'])) {
-    $element['#route_parameters'] = empty($element['#route_parameters']) ? array() : $element['#route_parameters'];
-    $element['#markup'] = \Drupal::linkGenerator()->generate($element['#title'], $element['#route_name'], $element['#route_parameters'], $element['#options']);
-  }
-  else {
-    $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']);
-  }
-  return $element;
+  return Element\Link::preRenderLink($element);
 }
 
 /**
diff --git a/core/includes/form.inc b/core/includes/form.inc
index de7bc39..9d3d506 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -687,22 +687,10 @@ function form_type_select_value($element, $input = FALSE) {
 /**
  * Determines the value for a textfield form element.
  *
- * @param $element
- *   The form element whose value is being populated.
- * @param $input
- *   The incoming input to populate the form element. If this is FALSE,
- *   the element's default value should be returned.
- *
- * @return
- *   The data that will appear in the $element_state['values'] collection
- *   for this element. Return nothing to use the default.
+ * @deprecated Use \Drupal\Core\Render\Element\Textfield::valueCallback().
  */
-function form_type_textfield_value($element, $input = FALSE) {
-  if ($input !== FALSE && $input !== NULL) {
-    // Equate $input to the form value to ensure it's marked for
-    // validation.
-    return str_replace(array("\r", "\n"), '', $input);
-  }
+function form_type_textfield_value(&$element, $input, &$form_state) {
+  return Element\Textfield::valueCallback($element, $input, $form_state)
 }
 
 /**
@@ -1427,24 +1415,10 @@ function form_pre_render_actions_dropbutton(array $element) {
 /**
  * #process callback for #pattern form element property.
  *
- * @param $element
- *   An associative array containing the properties and children of the
- *   generic input element.
- * @param $form_state
- *   The $form_state array for the form this element belongs to.
- *
- * @return
- *   The processed element.
- *
- * @see form_validate_pattern()
+ * @deprecated Use \Drupal\Core\Render\Element\FormElement::processPattern().
  */
-function form_process_pattern($element, &$form_state) {
-  if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) {
-    $element['#attributes']['pattern'] = $element['#pattern'];
-    $element['#element_validate'][] = 'form_validate_pattern';
-  }
-
-  return $element;
+function form_process_pattern(&$element, &$form_state, &$complete_form) {
+  return Element\FormElement::processPattern($element, $form_state, $complete_form);
 }
 
 /**
@@ -1829,181 +1803,28 @@ function form_validate_table($element, &$form_state) {
 /**
  * Processes a machine-readable name form element.
  *
- * @param $element
- *   The form element to process. Properties used:
- *   - #machine_name: An associative array containing:
- *     - exists: A callable to invoke for checking whether a submitted machine
- *       name value already exists. The submitted value is passed as an
- *       argument. In most cases, an existing API or menu argument loader
- *       function can be re-used. The callback is only invoked, if the submitted
- *       value differs from the element's #default_value.
- *     - source: (optional) The #array_parents of the form element containing
- *       the human-readable name (i.e., as contained in the $form structure) to
- *       use as source for the machine name. Defaults to array('label').
- *     - label: (optional) A text to display as label for the machine name value
- *       after the human-readable name form element. Defaults to "Machine name".
- *     - replace_pattern: (optional) A regular expression (without delimiters)
- *       matching disallowed characters in the machine name. Defaults to
- *       '[^a-z0-9_]+'.
- *     - replace: (optional) A character to replace disallowed characters in the
- *       machine name via JavaScript. Defaults to '_' (underscore). When using a
- *       different character, 'replace_pattern' needs to be set accordingly.
- *     - error: (optional) A custom form error message string to show, if the
- *       machine name contains disallowed characters.
- *     - standalone: (optional) Whether the live preview should stay in its own
- *       form element rather than in the suffix of the source element. Defaults
- *       to FALSE.
- *   - #maxlength: (optional) Should be set to the maximum allowed length of the
- *     machine name. Defaults to 64.
- *   - #disabled: (optional) Should be set to TRUE in case an existing machine
- *     name must not be changed after initial creation.
+ * @deprecated Use \Drupal\Core\Render\Element\MachineName::processMachineName().
  */
-function form_process_machine_name($element, &$form_state) {
-  // We need to pass the langcode to the client.
-  $language = \Drupal::languageManager()->getCurrentLanguage();
-
-  // Apply default form element properties.
-  $element += array(
-    '#title' => t('Machine-readable name'),
-    '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
-    '#machine_name' => array(),
-    '#field_prefix' => '',
-    '#field_suffix' => '',
-    '#suffix' => '',
-  );
-  // A form element that only wants to set one #machine_name property (usually
-  // 'source' only) would leave all other properties undefined, if the defaults
-  // were defined in hook_element_info(). Therefore, we apply the defaults here.
-  $element['#machine_name'] += array(
-    'source' => array('label'),
-    'target' => '#' . $element['#id'],
-    'label' => t('Machine name'),
-    'replace_pattern' => '[^a-z0-9_]+',
-    'replace' => '_',
-    'standalone' => FALSE,
-    'field_prefix' => $element['#field_prefix'],
-    'field_suffix' => $element['#field_suffix'],
-  );
-
-  // By default, machine names are restricted to Latin alphanumeric characters.
-  // So, default to LTR directionality.
-  if (!isset($element['#attributes'])) {
-    $element['#attributes'] = array();
-  }
-  $element['#attributes'] += array('dir' => 'ltr');
-
-  // The source element defaults to array('name'), but may have been overidden.
-  if (empty($element['#machine_name']['source'])) {
-    return $element;
-  }
-
-  // Retrieve the form element containing the human-readable name from the
-  // complete form in $form_state. By reference, because we may need to append
-  // a #field_suffix that will hold the live preview.
-  $key_exists = NULL;
-  $source = NestedArray::getValue($form_state['complete_form'], $element['#machine_name']['source'], $key_exists);
-  if (!$key_exists) {
-    return $element;
-  }
-
-  $suffix_id = $source['#id'] . '-machine-name-suffix';
-  $element['#machine_name']['suffix'] = '#' . $suffix_id;
-
-  if ($element['#machine_name']['standalone']) {
-    $element['#suffix'] = SafeMarkup::set($element['#suffix'] . ' <small id="' . $suffix_id . '">&nbsp;</small>');
-  }
-  else {
-    // Append a field suffix to the source form element, which will contain
-    // the live preview of the machine name.
-    $source += array('#field_suffix' => '');
-    $source['#field_suffix'] = SafeMarkup::set($source['#field_suffix'] . ' <small id="' . $suffix_id . '">&nbsp;</small>');
-
-    $parents = array_merge($element['#machine_name']['source'], array('#field_suffix'));
-    NestedArray::setValue($form_state['complete_form'], $parents, $source['#field_suffix']);
-  }
-
-  $js_settings = array(
-    'type' => 'setting',
-    'data' => array(
-      'machineName' => array(
-        '#' . $source['#id'] => $element['#machine_name'],
-      ),
-      'langcode' => $language->id,
-    ),
-  );
-  $element['#attached']['library'][] = 'core/drupal.machine-name';
-  $element['#attached']['js'][] = $js_settings;
-
-  return $element;
+function form_process_machine_name(&$element, &$form_state, &$complete_form) {
+  return Element\MachineName::processMachineName($element, $form_state, $complete_form);
 }
 
 /**
  * Form element validation handler for machine_name elements.
  *
- * Note that #maxlength is validated by _form_validate() already.
+ * @deprecated Use \Drupal\Core\Render\Element\MachineName::validateMachineName().
  */
-function form_validate_machine_name(&$element, &$form_state) {
-  // Verify that the machine name not only consists of replacement tokens.
-  if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) {
-    form_error($element, $form_state, t('The machine-readable name must contain unique characters.'));
-  }
-
-  // Verify that the machine name contains no disallowed characters.
-  if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) {
-    if (!isset($element['#machine_name']['error'])) {
-      // Since a hyphen is the most common alternative replacement character,
-      // a corresponding validation error message is supported here.
-      if ($element['#machine_name']['replace'] == '-') {
-        form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
-      }
-      // Otherwise, we assume the default (underscore).
-      else {
-        form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
-      }
-    }
-    else {
-      form_error($element, $form_state, $element['#machine_name']['error']);
-    }
-  }
-
-  // Verify that the machine name is unique.
-  if ($element['#default_value'] !== $element['#value']) {
-    $function = $element['#machine_name']['exists'];
-    if (call_user_func($function, $element['#value'], $element, $form_state)) {
-      form_error($element, $form_state, t('The machine-readable name is already in use. It must be unique.'));
-    }
-  }
+function form_validate_machine_name(&$element, &$form_state, &$complete_form) {
+  Element\MachineName::validateMachineName($element, $form_state, $complete_form);
 }
 
 /**
  * Arranges elements into groups.
  *
- * @param $element
- *   An associative array containing the properties and children of the
- *   element. Note that $element must be taken by reference here, so processed
- *   child elements are taken over into $form_state.
- * @param $form_state
- *   The $form_state array for the form this element belongs to.
- *
- * @return
- *   The processed element.
+ * @deprecated Use \Drupal\Core\Render\Element\FormElement::processGroup().
  */
-function form_process_group(&$element, &$form_state) {
-  $parents = implode('][', $element['#parents']);
-
-  // Each details element forms a new group. The #type 'vertical_tabs' basically
-  // only injects a new details element.
-  $form_state['groups'][$parents]['#group_exists'] = TRUE;
-  $element['#groups'] = &$form_state['groups'];
-
-  // Process vertical tabs group member details elements.
-  if (isset($element['#group'])) {
-    // Add this details element to the defined group (by reference).
-    $group = $element['#group'];
-    $form_state['groups'][$group][] = &$element;
-  }
-
-  return $element;
+function form_process_group(&$element, &$form_state, &$complete_form) {
+  return Element\FormElement::processGroup($element, $form_state, $complete_form);
 }
 
 /**
@@ -2043,61 +1864,10 @@ function form_pre_render_details($element) {
 /**
  * Adds members of this group as actual elements for rendering.
  *
- * @param $element
- *   An associative array containing the properties and children of the
- *   element.
- *
- * @return
- *   The modified element with all group members.
+ * @deprecated Use \Drupal\Core\Render\ElementElementBase::preRenderGroup().
  */
 function form_pre_render_group($element) {
-  // The element may be rendered outside of a Form API context.
-  if (!isset($element['#parents']) || !isset($element['#groups'])) {
-    return $element;
-  }
-
-  // Inject group member elements belonging to this group.
-  $parents = implode('][', $element['#parents']);
-  $children = Element::children($element['#groups'][$parents]);
-  if (!empty($children)) {
-    foreach ($children as $key) {
-      // Break references and indicate that the element should be rendered as
-      // group member.
-      $child = (array) $element['#groups'][$parents][$key];
-      $child['#group_details'] = TRUE;
-      // Inject the element as new child element.
-      $element[] = $child;
-
-      $sort = TRUE;
-    }
-    // Re-sort the element's children if we injected group member elements.
-    if (isset($sort)) {
-      $element['#sorted'] = FALSE;
-    }
-  }
-
-  if (isset($element['#group'])) {
-    // Contains form element summary functionalities.
-    $element['#attached']['library'][] = 'core/drupal.form';
-
-    $group = $element['#group'];
-    // If this element belongs to a group, but the group-holding element does
-    // not exist, we need to render it (at its original location).
-    if (!isset($element['#groups'][$group]['#group_exists'])) {
-      // Intentionally empty to clarify the flow; we simply return $element.
-    }
-    // If we injected this element into the group, then we want to render it.
-    elseif (!empty($element['#group_details'])) {
-      // Intentionally empty to clarify the flow; we simply return $element.
-    }
-    // Otherwise, this element belongs to a group and the group exists, so we do
-    // not render it.
-    elseif (Element::children($element['#groups'][$group])) {
-      $element['#printed'] = TRUE;
-    }
-  }
-
-  return $element;
+  return Element\RenderElement::preRenderGroup($element);
 }
 
 /**
@@ -2185,43 +1955,10 @@ function template_preprocess_vertical_tabs(&$variables) {
  * Adds autocomplete functionality to elements with a valid
  * #autocomplete_route_name.
  *
- * Suppose your autocomplete route name is 'mymodule.autocomplete' and its path
- * is: '/mymodule/autocomplete/{a}/{b}'
- * In your form you have:
- * @code
- * '#autocomplete_route_name' => 'mymodule.autocomplete',
- * '#autocomplete_route_parameters' => array('a' => $some_key, 'b' => $some_id),
- * @endcode
- * The user types in "keywords" so the full path called is:
- * 'mymodule_autocomplete/$some_key/$some_id?q=keywords'
- *
- * @param array $element
- *   The form element to process. Properties used:
- *   - #autocomplete_route_name: A route to be used as callback URL by the
- *     autocomplete JavaScript library.
- *   - #autocomplete_route_parameters: The parameters to be used in conjunction
- *     with the route name.
- * @param array $form_state
- *   An associative array containing the current state of the form.
- *
- * @return array
- *   The form element.
+ * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAutocomplete().
  */
-function form_process_autocomplete($element, &$form_state) {
-  $access = FALSE;
-  if (!empty($element['#autocomplete_route_name'])) {
-    $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
-
-    $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
-    $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
-  }
-  if ($access) {
-    $element['#attributes']['class'][] = 'form-autocomplete';
-    $element['#attached']['library'][] = 'core/drupal.autocomplete';
-    // Provide a data attribute for the JavaScript behavior to bind to.
-    $element['#attributes']['data-autocomplete-path'] = $path;
-  }
-  return $element;
+function form_process_autocomplete(&$element, &$form_state, &$complete_form) {
+  return Element\FormElement::processAutocomplete($element, $form_state, $complete_form);
 }
 
 /**
@@ -2327,20 +2064,10 @@ function form_pre_render_hidden($element) {
 /**
  * Prepares a #type 'textfield' render element for theme_input().
  *
- * @param array $element
- *   An associative array containing the properties of the element.
- *   Properties used: #title, #value, #description, #size, #maxlength,
- *   #placeholder, #required, #attributes.
- *
- * @return array
- *   The $element with prepared variables ready for theme_input().
+ * @deprecated Use \Drupal\Core\Render\Element\Textfield::preRenderTextfield().
  */
 function form_pre_render_textfield($element) {
-  $element['#attributes']['type'] = 'text';
-  Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
-  _form_set_attributes($element, array('form-text'));
-
-  return $element;
+  return Element\Textfield::preRenderTextfield($element);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index c150c29..5885eaa 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -1042,7 +1042,14 @@ protected function handleInputElement($form_id, &$element, &$form_state) {
 
     // Set the element's #value property.
     if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
+      // @todo Once all elements are converted to plugins in
+      //   https://www.drupal.org/node/2311393, rely on
+      //   $element['#value_callback'] directly.
       $value_callable = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value';
+      if (!is_callable($value_callable)) {
+        $value_callable = '\Drupal\Core\Render\Element\FormElement::valueCallback';
+      }
+
       if ($process_input) {
         // Get the input for the current element. NULL values in the input need
         // to be explicitly distinguished from missing input. (see below)
@@ -1066,9 +1073,8 @@ protected function handleInputElement($form_id, &$element, &$form_state) {
         // If we have input for the current element, assign it to the #value
         // property, optionally filtered through $value_callback.
         if ($input_exists) {
-          if (is_callable($value_callable)) {
-            $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state));
-          }
+          $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state));
+
           if (!isset($element['#value']) && isset($input)) {
             $element['#value'] = $input;
           }
@@ -1082,9 +1088,8 @@ protected function handleInputElement($form_id, &$element, &$form_state) {
       if (!isset($element['#value'])) {
         // Call #type_value without a second argument to request default_value
         // handling.
-        if (is_callable($value_callable)) {
-          $element['#value'] = call_user_func_array($value_callable, array(&$element, FALSE, &$form_state));
-        }
+        $element['#value'] = call_user_func_array($value_callable, array(&$element, FALSE, &$form_state));
+
         // Final catch. If we haven't set a value yet, use the explicit default
         // value. Avoid image buttons (which come with garbage value), so we
         // only get value for the button actually clicked.
diff --git a/core/lib/Drupal/Core/Render/Annotation/FormElement.php b/core/lib/Drupal/Core/Render/Annotation/FormElement.php
new file mode 100644
index 0000000..b7288a0
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Annotation/FormElement.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Annotation\FormElement.
+ */
+
+namespace Drupal\Core\Render\Annotation;
+
+/**
+ * Defines a form element plugin annotation object.
+ *
+ * Form elements are also valid render elements, but they specifically represent
+ * an HTML element that takes input as part of a form. It will be discovered via
+ * the same mechanism as regular render element plugins.
+ *
+ * Plugin Namespace: Element
+ *
+ * For a working example, see \Drupal\Core\Render\Element\Textfield.
+ *
+ * @see \Drupal\Core\Render\ElementInfoManager
+ * @see \Drupal\Core\Render\ElementInterface
+ * @see \Drupal\Core\Render\FormElement
+ *
+ * @ingroup plugin_api
+ *
+ * @Annotation
+ */
+class FormElement extends RenderElement {
+
+}
diff --git a/core/lib/Drupal/Core/Render/Annotation/RenderElement.php b/core/lib/Drupal/Core/Render/Annotation/RenderElement.php
new file mode 100644
index 0000000..cb03875
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Annotation/RenderElement.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Annotation\RenderElement.
+ */
+
+namespace Drupal\Core\Render\Annotation;
+
+use Drupal\Component\Annotation\PluginID;
+
+/**
+ * Defines a render element plugin annotation object.
+ *
+ * Render element plugins allow modules to declare their own Render API element
+ * types and specify their default values.
+ *
+ * This annotation allows modules to declare their own render element types and
+ * to specify their default values. The values returned by this plugin will be
+ * merged with the elements returned by render arrays and so can return defaults
+ * for any Render APIs keys in addition to those explicitly documented by
+ * \Drupal\Core\Render\ElementInfoManagerInterface::getInfo().
+ *
+ * Plugin Namespace: Element
+ *
+ * For a working example, see \Drupal\Core\Render\Element\Link.
+ *
+ * @see \Drupal\Core\Render\ElementInfoManager
+ * @see \Drupal\Core\Render\ElementInterface
+ * @see \Drupal\Core\Render\RenderElement
+ *
+ * @ingroup plugin_api
+ *
+ * @Annotation
+ */
+class RenderElement extends PluginID {
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/ElementInterface.php b/core/lib/Drupal/Core/Render/Element/ElementInterface.php
new file mode 100644
index 0000000..020e18a
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/ElementInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\ElementInterface.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Provides an interface for element plugins.
+ *
+ * @see \Drupal\Core\Render\ElementInfoManager
+ * @see \Drupal\Core\Render\Annotation\RenderElement
+ * @see \Drupal\Core\Render\Annotation\FormElement
+ * @see \Drupal\Core\Render\RenderElement
+ * @see \Drupal\Core\Render\FormElement
+ */
+interface ElementInterface extends PluginInspectionInterface {
+
+  /**
+   * Returns the element properties for this element.
+   *
+   * @return array
+   *   An array of element properties.
+   */
+  public function getInfo();
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/FormElement.php b/core/lib/Drupal/Core/Render/Element/FormElement.php
new file mode 100644
index 0000000..210af9a
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/FormElement.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\FormElement.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+/**
+ * Provides a base class for form render plugins.
+ *
+ * @see \Drupal\Core\Render\Annotation\FormElement
+ * @see \Drupal\Core\Render\ElementInterface
+ * @see \Drupal\Core\Render\ElementInfoManager
+ *
+ * @ingroup plugin_api
+ */
+abstract class FormElement extends RenderElement implements FormElementInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, &$form_state) {
+    return NULL;
+  }
+
+  /**
+   * Form element processing handler for the #ajax form property.
+   *
+   * @param array $element
+   *   An associative array containing the properties of the element.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The processed element.
+   *
+   * @see ajax_pre_render_element()
+   */
+  public static function processAjaxForm(&$element, &$form_state, &$complete_form) {
+    $element = ajax_pre_render_element($element);
+    if (!empty($element['#ajax_processed'])) {
+      $form_state['cache'] = TRUE;
+    }
+    return $element;
+  }
+
+  /**
+   * Arranges elements into groups.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of the
+   *   element. Note that $element must be taken by reference here, so processed
+   *   child elements are taken over into $form_state.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The processed element.
+   */
+  public static function processGroup(&$element, &$form_state, &$complete_form) {
+    $parents = implode('][', $element['#parents']);
+
+    // Each details element forms a new group. The #type 'vertical_tabs' basically
+    // only injects a new details element.
+    $form_state['groups'][$parents]['#group_exists'] = TRUE;
+    $element['#groups'] = &$form_state['groups'];
+
+    // Process vertical tabs group member details elements.
+    if (isset($element['#group'])) {
+      // Add this details element to the defined group (by reference).
+      $group = $element['#group'];
+      $form_state['groups'][$group][] = &$element;
+    }
+
+    return $element;
+  }
+
+  /**
+   * #process callback for #pattern form element property.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of the
+   *   generic input element.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The processed element.
+   *
+   * @see form_validate_pattern()
+   */
+  public static function processPattern(&$element, &$form_state, &$complete_form) {
+    if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) {
+      $element['#attributes']['pattern'] = $element['#pattern'];
+      $element['#element_validate'][] = 'form_validate_pattern';
+    }
+
+    return $element;
+  }
+
+  /**
+   * Adds autocomplete functionality to elements with a valid
+   * #autocomplete_route_name.
+   *
+   * Suppose your autocomplete route name is 'mymodule.autocomplete' and its path
+   * is: '/mymodule/autocomplete/{a}/{b}'
+   * In your form you have:
+   * @code
+   * '#autocomplete_route_name' => 'mymodule.autocomplete',
+   * '#autocomplete_route_parameters' => array('a' => $some_key, 'b' => $some_id),
+   * @endcode
+   * The user types in "keywords" so the full path called is:
+   * 'mymodule_autocomplete/$some_key/$some_id?q=keywords'
+   *
+   * @param array $element
+   *   The form element to process. Properties used:
+   *   - #autocomplete_route_name: A route to be used as callback URL by the
+   *     autocomplete JavaScript library.
+   *   - #autocomplete_route_parameters: The parameters to be used in conjunction
+   *     with the route name.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The form element.
+   */
+  public static function processAutocomplete(&$element, &$form_state, &$complete_form) {
+    $access = FALSE;
+    if (!empty($element['#autocomplete_route_name'])) {
+      $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
+
+      $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
+      $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
+    }
+    if ($access) {
+      $element['#attributes']['class'][] = 'form-autocomplete';
+      $element['#attached']['library'][] = 'core/drupal.autocomplete';
+      // Provide a data attribute for the JavaScript behavior to bind to.
+      $element['#attributes']['data-autocomplete-path'] = $path;
+    }
+
+    return $element;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/FormElementInterface.php b/core/lib/Drupal/Core/Render/Element/FormElementInterface.php
new file mode 100644
index 0000000..992d94b
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/FormElementInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\FormElementInterface.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+/**
+ * Provides an interface for form element plugins.
+ */
+interface FormElementInterface extends ElementInterface {
+
+  /**
+   * Determines how user input is mapped to an element's #value property.
+   *
+   * @param array $element
+   *   An associative array containing the properties of the element.
+   * @param mixed $input
+   *   The incoming input to populate the form element. If this is FALSE,
+   *   the element's default value should be returned.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return mixed
+   *   The value to assign to the element.
+   */
+  public static function valueCallback(&$element, $input, &$form_state);
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php
new file mode 100644
index 0000000..379b0a7
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Link.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\Link.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+/**
+ * Provides a link element.
+ *
+ * @RenderElement("link")
+ */
+class Link extends RenderElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      '#pre_render' => array(
+        array($class, 'preRenderLink'),
+      ),
+    );
+  }
+
+  /**
+   * Pre-render callback: Renders a link into #markup.
+   *
+   * Doing so during pre_render gives modules a chance to alter the link parts.
+   *
+   * @param array $element
+   *   A structured array whose keys form the arguments to l():
+   *   - #title: The link text to pass as argument to l().
+   *   - One of the following
+   *     - #route_name and (optionally) and a #route_parameters array; The route
+   *       name and route parameters which will be passed into the link generator.
+   *     - #href: The system path or URL to pass as argument to l().
+   *   - #options: (optional) An array of options to pass to l() or the link
+   *     generator.
+   *
+   * @return array
+   *   The passed-in element containing a rendered link in '#markup'.
+   */
+  public static function preRenderLink($element) {
+    // By default, link options to pass to l() are normally set in #options.
+    $element += array('#options' => array());
+    // However, within the scope of renderable elements, #attributes is a valid
+    // way to specify attributes, too. Take them into account, but do not override
+    // attributes from #options.
+    if (isset($element['#attributes'])) {
+      $element['#options'] += array('attributes' => array());
+      $element['#options']['attributes'] += $element['#attributes'];
+    }
+
+    // This #pre_render callback can be invoked from inside or outside of a Form
+    // API context, and depending on that, a HTML ID may be already set in
+    // different locations. #options should have precedence over Form API's #id.
+    // #attributes have been taken over into #options above already.
+    if (isset($element['#options']['attributes']['id'])) {
+      $element['#id'] = $element['#options']['attributes']['id'];
+    }
+    elseif (isset($element['#id'])) {
+      $element['#options']['attributes']['id'] = $element['#id'];
+    }
+
+    // Conditionally invoke ajax_pre_render_element(), if #ajax is set.
+    if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) {
+      // If no HTML ID was found above, automatically create one.
+      if (!isset($element['#id'])) {
+        $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link');
+      }
+      // If #ajax['path] was not specified, use the href as Ajax request URL.
+      if (!isset($element['#ajax']['path'])) {
+        $element['#ajax']['path'] = $element['#href'];
+        $element['#ajax']['options'] = $element['#options'];
+      }
+      $element = ajax_pre_render_element($element);
+    }
+
+    if (isset($element['#route_name'])) {
+      $element['#route_parameters'] = empty($element['#route_parameters']) ? array() : $element['#route_parameters'];
+      $element['#markup'] = \Drupal::linkGenerator()->generate($element['#title'], $element['#route_name'], $element['#route_parameters'], $element['#options']);
+    }
+    else {
+      $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']);
+    }
+    return $element;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php
new file mode 100644
index 0000000..f318262
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/MachineName.php
@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\MachineName.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\SafeMarkup;
+
+/**
+ * Format a single-line text field to store a unique machine-readable name.
+ *
+ * Provides a form element to enter a machine name, which is validated to ensure
+ * that the name is unique and does not contain disallowed characters. All
+ * disallowed characters are replaced with a replacement character via
+ * JavaScript.
+ *
+ * @FormElement("machine_name")
+ */
+class MachineName extends Textfield {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      '#input' => TRUE,
+      '#default_value' => NULL,
+      '#required' => TRUE,
+      '#maxlength' => 64,
+      '#size' => 60,
+      '#autocomplete_route_name' => FALSE,
+      '#process' => array(
+        array($class, 'processMachineName'),
+        array($class, 'processAutocomplete'),
+        array($class, 'processAjaxForm'),
+      ),
+      '#element_validate' => array(
+        array($class, 'validateMachineName'),
+      ),
+      '#pre_render' => array(
+        array($class, 'preRenderTextfield'),
+      ),
+      '#theme' => 'input__textfield',
+      '#theme_wrappers' => array('form_element'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, &$form_state) {
+    return NULL;
+  }
+
+  /**
+   * Processes a machine-readable name form element.
+   *
+   * @param array $element
+   *   The form element to process. Properties used:
+   *   - #machine_name: An associative array containing:
+   *     - exists: A callable to invoke for checking whether a submitted machine
+   *       name value already exists. The submitted value is passed as an
+   *       argument. In most cases, an existing API or menu argument loader
+   *       function can be re-used. The callback is only invoked, if the submitted
+   *       value differs from the element's #default_value.
+   *     - source: (optional) The #array_parents of the form element containing
+   *       the human-readable name (i.e., as contained in the $form structure) to
+   *       use as source for the machine name. Defaults to array('label').
+   *     - label: (optional) A text to display as label for the machine name value
+   *       after the human-readable name form element. Defaults to "Machine name".
+   *     - replace_pattern: (optional) A regular expression (without delimiters)
+   *       matching disallowed characters in the machine name. Defaults to
+   *       '[^a-z0-9_]+'.
+   *     - replace: (optional) A character to replace disallowed characters in the
+   *       machine name via JavaScript. Defaults to '_' (underscore). When using a
+   *       different character, 'replace_pattern' needs to be set accordingly.
+   *     - error: (optional) A custom form error message string to show, if the
+   *       machine name contains disallowed characters.
+   *     - standalone: (optional) Whether the live preview should stay in its own
+   *       form element rather than in the suffix of the source element. Defaults
+   *       to FALSE.
+   *   - #maxlength: (optional) Should be set to the maximum allowed length of the
+   *     machine name. Defaults to 64.
+   *   - #disabled: (optional) Should be set to TRUE in case an existing machine
+   *     name must not be changed after initial creation.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The processed element.
+   */
+  public static function processMachineName(&$element, &$form_state, &$complete_form) {
+    // We need to pass the langcode to the client.
+    $language = \Drupal::languageManager()->getCurrentLanguage();
+
+    // Apply default form element properties.
+    $element += array(
+      '#title' => t('Machine-readable name'),
+      '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
+      '#machine_name' => array(),
+      '#field_prefix' => '',
+      '#field_suffix' => '',
+      '#suffix' => '',
+    );
+    // A form element that only wants to set one #machine_name property (usually
+    // 'source' only) would leave all other properties undefined, if the defaults
+    // were defined in hook_element_info(). Therefore, we apply the defaults here.
+    $element['#machine_name'] += array(
+      'source' => array('label'),
+      'target' => '#' . $element['#id'],
+      'label' => t('Machine name'),
+      'replace_pattern' => '[^a-z0-9_]+',
+      'replace' => '_',
+      'standalone' => FALSE,
+      'field_prefix' => $element['#field_prefix'],
+      'field_suffix' => $element['#field_suffix'],
+    );
+
+    // By default, machine names are restricted to Latin alphanumeric characters.
+    // So, default to LTR directionality.
+    if (!isset($element['#attributes'])) {
+      $element['#attributes'] = array();
+    }
+    $element['#attributes'] += array('dir' => 'ltr');
+
+    // The source element defaults to array('name'), but may have been overidden.
+    if (empty($element['#machine_name']['source'])) {
+      return $element;
+    }
+
+    // Retrieve the form element containing the human-readable name from the
+    // complete form in $form_state. By reference, because we may need to append
+    // a #field_suffix that will hold the live preview.
+    $key_exists = NULL;
+    $source = NestedArray::getValue($form_state['complete_form'], $element['#machine_name']['source'], $key_exists);
+    if (!$key_exists) {
+      return $element;
+    }
+
+    $suffix_id = $source['#id'] . '-machine-name-suffix';
+    $element['#machine_name']['suffix'] = '#' . $suffix_id;
+
+    if ($element['#machine_name']['standalone']) {
+      $element['#suffix'] = SafeMarkup::set($element['#suffix'] . ' <small id="' . $suffix_id . '">&nbsp;</small>');
+    }
+    else {
+      // Append a field suffix to the source form element, which will contain
+      // the live preview of the machine name.
+      $source += array('#field_suffix' => '');
+      $source['#field_suffix'] = SafeMarkup::set($source['#field_suffix'] . ' <small id="' . $suffix_id . '">&nbsp;</small>');
+
+      $parents = array_merge($element['#machine_name']['source'], array('#field_suffix'));
+      NestedArray::setValue($form_state['complete_form'], $parents, $source['#field_suffix']);
+    }
+
+    $js_settings = array(
+      'type' => 'setting',
+      'data' => array(
+        'machineName' => array(
+          '#' . $source['#id'] => $element['#machine_name'],
+        ),
+        'langcode' => $language->id,
+      ),
+    );
+    $element['#attached']['library'][] = 'core/drupal.machine-name';
+    $element['#attached']['js'][] = $js_settings;
+
+    return $element;
+  }
+
+  /**
+   * Form element validation handler for machine_name elements.
+   *
+   * Note that #maxlength is validated by _form_validate() already.
+   *
+   * This checks that the submitted value
+   * - does not contain the replacement character only
+   * - does not contain disallowed characters
+   * - is unique; i.e., does not already exist
+   * - does not exceed the maximum length (via #maxlength)
+   * - cannot be changed after creation (via #disabled)
+   */
+  public static function validateMachineName(&$element, &$form_state, &$complete_form) {
+    // Verify that the machine name not only consists of replacement tokens.
+    if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) {
+      form_error($element, $form_state, t('The machine-readable name must contain unique characters.'));
+    }
+
+    // Verify that the machine name contains no disallowed characters.
+    if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) {
+      if (!isset($element['#machine_name']['error'])) {
+        // Since a hyphen is the most common alternative replacement character,
+        // a corresponding validation error message is supported here.
+        if ($element['#machine_name']['replace'] == '-') {
+          form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
+        }
+        // Otherwise, we assume the default (underscore).
+        else {
+          form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
+        }
+      }
+      else {
+        form_error($element, $form_state, $element['#machine_name']['error']);
+      }
+    }
+
+    // Verify that the machine name is unique.
+    if ($element['#default_value'] !== $element['#value']) {
+      $function = $element['#machine_name']['exists'];
+      if (call_user_func($function, $element['#value'], $element, $form_state)) {
+        form_error($element, $form_state, t('The machine-readable name is already in use. It must be unique.'));
+      }
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
new file mode 100644
index 0000000..19cb772
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\RenderElement.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Render\Element;
+
+/**
+ * Provides a base class for element render plugins.
+ *
+ * @see \Drupal\Core\Render\Annotation\RenderElement
+ * @see \Drupal\Core\Render\ElementInterface
+ * @see \Drupal\Core\Render\ElementInfoManager
+ *
+ * @ingroup plugin_api
+ */
+abstract class RenderElement extends PluginBase implements ElementInterface {
+
+  /**
+   * Adds members of this group as actual elements for rendering.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of the
+   *   element.
+   *
+   * @return array
+   *   The modified element with all group members.
+   */
+  public static function preRenderGroup($element) {
+    // The element may be rendered outside of a Form API context.
+    if (!isset($element['#parents']) || !isset($element['#groups'])) {
+      return $element;
+    }
+
+    // Inject group member elements belonging to this group.
+    $parents = implode('][', $element['#parents']);
+    $children = Element::children($element['#groups'][$parents]);
+    if (!empty($children)) {
+      foreach ($children as $key) {
+        // Break references and indicate that the element should be rendered as
+        // group member.
+        $child = (array) $element['#groups'][$parents][$key];
+        $child['#group_details'] = TRUE;
+        // Inject the element as new child element.
+        $element[] = $child;
+
+        $sort = TRUE;
+      }
+      // Re-sort the element's children if we injected group member elements.
+      if (isset($sort)) {
+        $element['#sorted'] = FALSE;
+      }
+    }
+
+    if (isset($element['#group'])) {
+      // Contains form element summary functionalities.
+      $element['#attached']['library'][] = 'core/drupal.form';
+
+      $group = $element['#group'];
+      // If this element belongs to a group, but the group-holding element does
+      // not exist, we need to render it (at its original location).
+      if (!isset($element['#groups'][$group]['#group_exists'])) {
+        // Intentionally empty to clarify the flow; we simply return $element.
+      }
+      // If we injected this element into the group, then we want to render it.
+      elseif (!empty($element['#group_details'])) {
+        // Intentionally empty to clarify the flow; we simply return $element.
+      }
+      // Otherwise, this element belongs to a group and the group exists, so we do
+      // not render it.
+      elseif (Element::children($element['#groups'][$group])) {
+        $element['#printed'] = TRUE;
+      }
+    }
+
+    return $element;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/Textfield.php b/core/lib/Drupal/Core/Render/Element/Textfield.php
new file mode 100644
index 0000000..75b4c4c
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Textfield.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\Textfield.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Core\Render\Element;
+
+/**
+ * Formats a single-line text field.
+ *
+ * @FormElement("textfield")
+ */
+class Textfield extends FormElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      '#input' => TRUE,
+      '#size' => 60,
+      '#maxlength' => 128,
+      '#autocomplete_route_name' => FALSE,
+      '#process' => array(
+        array($class, 'processAutocomplete'),
+        array($class, 'processAjaxForm'),
+        array($class, 'processPattern'),
+        array($class, 'processGroup'),
+      ),
+      '#pre_render' => array(
+        array($class, 'preRenderTextfield'),
+        array($class, 'preRenderGroup'),
+      ),
+      '#theme' => 'input__textfield',
+      '#theme_wrappers' => array('form_element'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, &$form_state) {
+    if ($input !== FALSE && $input !== NULL) {
+      // Equate $input to the form value to ensure it's marked for
+      // validation.
+      return str_replace(array("\r", "\n"), '', $input);
+    }
+  }
+
+  /**
+   * Prepares a #type 'textfield' render element for theme_input().
+   *
+   * @param array $element
+   *   An associative array containing the properties of the element.
+   *   Properties used: #title, #value, #description, #size, #maxlength,
+   *   #placeholder, #required, #attributes.
+   *
+   * @return array
+   *   The $element with prepared variables ready for theme_input().
+   */
+  public static function preRenderTextfield($element) {
+    $element['#attributes']['type'] = 'text';
+    Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
+    _form_set_attributes($element, array('form-text'));
+
+    return $element;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/ElementInfo.php b/core/lib/Drupal/Core/Render/ElementInfo.php
deleted file mode 100644
index 8d6a3f2..0000000
--- a/core/lib/Drupal/Core/Render/ElementInfo.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Render\ElementInfo.
- */
-
-namespace Drupal\Core\Render;
-
-use Drupal\Core\Extension\ModuleHandlerInterface;
-
-/**
- * Provides the default element info implementation.
- */
-class ElementInfo implements ElementInfoInterface {
-
-  /**
-   * The module handler.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   */
-  protected $moduleHandler;
-
-  /**
-   * Stores the available element information
-   *
-   * @var array
-   */
-  protected $elementInfo;
-
-  /**
-   * Constructs a new ElementInfo instance.
-   *
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   */
-  public function __construct(ModuleHandlerInterface $module_handler) {
-    $this->moduleHandler = $module_handler;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getInfo($type) {
-    if (!isset($this->elementInfo)) {
-      $this->elementInfo = $this->buildInfo();
-    }
-    return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
-  }
-
-  /**
-   * Builds up all element information.
-   */
-  protected function buildInfo() {
-    $info = $this->moduleHandler->invokeAll('element_info');
-    foreach ($info as $element_type => $element) {
-      $info[$element_type]['#type'] = $element_type;
-    }
-    // Allow modules to alter the element type defaults.
-    $this->moduleHandler->alter('element_info', $info);
-
-    return $info;
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Render/ElementInfoManager.php b/core/lib/Drupal/Core/Render/ElementInfoManager.php
new file mode 100644
index 0000000..4756903
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/ElementInfoManager.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\ElementInfoManager.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Render\Element\FormElementInterface;
+
+/**
+ * Provides a plugin manager for element plugins.
+ *
+ * @see \Drupal\Core\Render\ElementInterface
+ * @see \Drupal\Core\Render\Annotation\RenderElement
+ * @see \Drupal\Core\Render\Annotation\FormElement
+ * @see \Drupal\Core\Render\RenderElement
+ * @see \Drupal\Core\Render\FormElement
+ *
+ * @ingroup plugin_api
+ */
+class ElementInfoManager extends DefaultPluginManager implements ElementInfoManagerInterface {
+
+  /**
+   * Stores the available element information.
+   *
+   * @var array
+   */
+  protected $elementInfo;
+
+  /**
+   * Constructs a ElementInfoManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    $this->setCacheBackend($cache_backend, 'element_info');
+
+    parent::__construct('Element', $namespaces, $module_handler, 'Drupal\Core\Render\Annotation\RenderElement');
+  }
+
+  public function getInfo($type) {
+    if (!isset($this->elementInfo)) {
+      $this->elementInfo = $this->buildInfo();
+    }
+    return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
+  }
+
+  /**
+   * Builds up all element information.
+   */
+  protected function buildInfo() {
+    // @todo Remove this hook once all elements are converted to plugins in
+    //   https://www.drupal.org/node/2311393.
+    $info = $this->moduleHandler->invokeAll('element_info');
+
+    foreach ($this->getDefinitions() as $element_type => $definition) {
+      $element = $this->createInstance($element_type);
+      $element_info = $element->getInfo();
+
+      // If this is element is to be used exclusively in a form, denote that it
+      // will receive input, and assign the value callback.
+      if ($element instanceof FormElementInterface) {
+        $element_info['#input'] = TRUE;
+        $element_info['#value_callback'] = array(get_class($element), 'valueCallback');
+      }
+      $info[$element_type] = $element_info;
+    }
+    foreach ($info as $element_type => $element) {
+      $info[$element_type]['#type'] = $element_type;
+    }
+    // Allow modules to alter the element type defaults.
+    $this->moduleHandler->alter('element_info', $info);
+
+    return $info;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Render\Element\ElementInterface
+   */
+  public function createInstance($plugin_id, array $configuration = array()) {
+    return parent::createInstance($plugin_id, $configuration);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/ElementInfoInterface.php b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php
similarity index 94%
rename from core/lib/Drupal/Core/Render/ElementInfoInterface.php
rename to core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php
index ad05aeb..423965f 100644
--- a/core/lib/Drupal/Core/Render/ElementInfoInterface.php
+++ b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Contains \Drupal\Core\Render\ElementInfoInterface.
+ * Contains \Drupal\Core\Render\ElementInfoManagerInterface.
  */
 
 namespace Drupal\Core\Render;
@@ -10,7 +10,7 @@
 /**
  * Defines available render array element types.
  */
-interface ElementInfoInterface {
+interface ElementInfoManagerInterface {
 
   /**
    * Retrieves the default properties for the defined element type.
diff --git a/core/lib/Drupal/Core/Render/Plugin/README.txt b/core/lib/Drupal/Core/Render/Plugin/README.txt
new file mode 100644
index 0000000..f761e01
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Plugin/README.txt
@@ -0,0 +1,4 @@
+@todo This must be here because DrupalKernel will only allow namespaces to
+  provide plugins if there is a Plugin subdirectory, and git does not allow
+  empty subdirectories. This file should be removed once
+  https://www.drupal.org/node/2309889 is fixed.
diff --git a/core/modules/color/color.module b/core/modules/color/color.module
index f0137cf..8df18e1 100644
--- a/core/modules/color/color.module
+++ b/core/modules/color/color.module
@@ -4,6 +4,7 @@
  * Allows users to change the color scheme of themes.
  */
 
+use Drupal\color\Element\ColorPalette;
 use Drupal\Core\Asset\CssOptimizer;
 use Drupal\Component\Utility\Bytes;
 use Drupal\Component\Utility\Environment;
@@ -234,12 +235,9 @@ function color_scheme_form($complete_form, &$form_state, $theme) {
   foreach ($palette as $name => $value) {
     if (isset($names[$name])) {
       $form['palette'][$name] = array(
-        '#type' => 'textfield',
+        '#type' => 'color_palette',
         '#title' => String::checkPlain($names[$name]),
-        '#value_callback' => 'color_palette_color_value',
         '#default_value' => $value,
-        '#size' => 8,
-        '#attributes' => array('dir' => 'ltr'),
       );
     }
   }
@@ -281,33 +279,10 @@ function template_preprocess_color_scheme_form(&$variables) {
 /**
  * Determines the value for a palette color field.
  *
- * @param array $element
- *   The form element whose value is being populated.
- * @param string|bool $input
- *   The incoming input to populate the form element. If this is FALSE,
- *   the element's default value should be returned.
- * @param array $form_state
- *   A keyed array containing the current state of the form.
- *
- * @return string
- *   The data that will appear in the $form_state['values'] collection for this
- *   element. Return nothing to use the default.
+ * @deprecated Use \Drupal\color\Element\ColorPalette::valueCallback().
  */
 function color_palette_color_value($element, $input = FALSE, $form_state = array()) {
-  // If we suspect a possible cross-site request forgery attack, only accept
-  // hexadecimal CSS color strings from user input, to avoid problems when this
-  // value is used in the JavaScript preview.
-  if ($input !== FALSE) {
-    // Start with the provided value for this textfield, and validate that if
-    // necessary, falling back on the default value.
-    $value = form_type_textfield_value($element, $input, $form_state);
-    if (!$value || !isset($form_state['complete form']['#token']) || color_valid_hexadecimal_string($value) || \Drupal::csrfToken()->validate($form_state['values']['form_token'], $form_state['complete form']['#token'])) {
-      return $value;
-    }
-    else {
-      return $element['#default_value'];
-    }
-  }
+  return ColorPalette::valueCallback($element, $input, $form_state);
 }
 
 /**
diff --git a/core/modules/color/src/Element/ColorPalette.php b/core/modules/color/src/Element/ColorPalette.php
new file mode 100644
index 0000000..d147412
--- /dev/null
+++ b/core/modules/color/src/Element/ColorPalette.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\color\Element\ColorPalette.
+ */
+
+namespace Drupal\color\Element;
+
+use Drupal\Core\Render\Element\Textfield;
+
+/**
+ * Provides a color palette form element.
+ *
+ * @FormElement("color_palette")
+ */
+class ColorPalette extends Textfield {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return array(
+      '#size' => 8,
+      '#attributes' => array('dir' => 'ltr'),
+    ) + parent::getInfo();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, &$form_state) {
+    // If we suspect a possible cross-site request forgery attack, only accept
+    // hexadecimal CSS color strings from user input, to avoid problems when this
+    // value is used in the JavaScript preview.
+    if ($input !== FALSE) {
+      // Start with the provided value for this textfield, and validate that if
+      // necessary, falling back on the default value.
+      $value = parent::valueCallback($element, $input, $form_state);
+      if (!$value || !isset($form_state['complete form']['#token']) || color_valid_hexadecimal_string($value) || \Drupal::csrfToken()->validate($form_state['values']['form_token'], $form_state['complete form']['#token'])) {
+        return $value;
+      }
+      else {
+        return $element['#default_value'];
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 111ddfe..6cb9e2c 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -193,11 +193,14 @@ function callback_queue_worker($queue_item_data) {
  * specify their default values. The values returned by this hook will be
  * merged with the elements returned by form constructor implementations and so
  * can return defaults for any Form APIs keys in addition to those explicitly
- * documented by \Drupal\Core\Render\ElementInfoInterface::getInfo().
+ * documented by \Drupal\Core\Render\ElementInfoManagerInterface::getInfo().
  *
  * @return array
  *   An associative array with structure identical to that of the return value
- *   of \Drupal\Core\Render\ElementInfoInterface::getInfo().
+ *   of \Drupal\Core\Render\ElementInfoManagerInterface::getInfo().
+ *
+ * @deprecated Use an annotated class instead, see
+ *   \Drupal\Core\Render\Element\ElementInterface.
  *
  * @see hook_element_info_alter()
  * @see system_element_info()
@@ -217,7 +220,7 @@ function hook_element_info() {
  *
  * @param array $types
  *   An associative array with structure identical to that of the return value
- *   of \Drupal\Core\Render\ElementInfoInterface::getInfo().
+ *   of \Drupal\Core\Render\ElementInfoManagerInterface::getInfo().
  *
  * @see hook_element_info()
  */
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 117dc49..fbc3dc2 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -360,16 +360,6 @@ function system_element_info() {
     '#pre_render' => array('form_pre_render_image_button'),
     '#theme_wrappers' => array('input__image_button'),
   );
-  $types['textfield'] = array(
-    '#input' => TRUE,
-    '#size' => 60,
-    '#maxlength' => 128,
-    '#autocomplete_route_name' => FALSE,
-    '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern', 'form_process_group'),
-    '#pre_render' => array('form_pre_render_textfield', 'form_pre_render_group'),
-    '#theme' => 'input__textfield',
-    '#theme_wrappers' => array('form_element'),
-  );
   $types['tel'] = array(
     '#input' => TRUE,
     '#size' => 30,
@@ -440,19 +430,6 @@ function system_element_info() {
     '#theme' => 'input__color',
     '#theme_wrappers' => array('form_element'),
   );
-  $types['machine_name'] = array(
-    '#input' => TRUE,
-    '#default_value' => NULL,
-    '#required' => TRUE,
-    '#maxlength' => 64,
-    '#size' => 60,
-    '#autocomplete_route_name' => FALSE,
-    '#process' => array('form_process_machine_name', 'form_process_autocomplete', 'ajax_process_form'),
-    '#element_validate' => array('form_validate_machine_name'),
-    '#pre_render' => array('form_pre_render_textfield'),
-    '#theme' => 'input__textfield',
-    '#theme_wrappers' => array('form_element'),
-  );
   $types['password'] = array(
     '#input' => TRUE,
     '#size' => 60,
@@ -578,9 +555,6 @@ function system_element_info() {
   $types['value'] = array(
     '#input' => TRUE,
   );
-  $types['link'] = array(
-    '#pre_render' => array('drupal_pre_render_link'),
-  );
   $types['fieldset'] = array(
     '#value' => NULL,
     '#process' => array('form_process_group', 'ajax_process_form'),
diff --git a/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php b/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php
index 86878f4..4479566 100644
--- a/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php
@@ -7,12 +7,11 @@
 
 namespace Drupal\Tests\Core\Render;
 
-use Drupal\Core\Render\ElementInfo;
-use Drupal\Core\Render\ElementInfoInterface;
+use Drupal\Core\Render\ElementInfoManager;
 use Drupal\Tests\UnitTestCase;
 
 /**
- * @coversDefaultClass \Drupal\Core\Render\ElementInfo
+ * @coversDefaultClass \Drupal\Core\Render\ElementInfoManager
  * @group Render
  */
 class ElementInfoTest extends UnitTestCase {
@@ -20,11 +19,18 @@ class ElementInfoTest extends UnitTestCase {
   /**
    * The class under test.
    *
-   * @var \Drupal\Core\Render\ElementInfoInterface
+   * @var \Drupal\Core\Render\ElementInfoManagerInterface
    */
   protected $elementInfo;
 
   /**
+   * The cache backend to use.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $cache;
+
+  /**
    * The mocked module handler.
    *
    * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
@@ -37,9 +43,10 @@ class ElementInfoTest extends UnitTestCase {
    * @covers ::__construct
    */
   protected function setUp() {
+    $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
 
-    $this->elementInfo = new ElementInfo($this->moduleHandler);
+    $this->elementInfo = new ElementInfoManager(new \ArrayObject(), $this->cache, $this->moduleHandler);
   }
 
   /**
