diff --git a/captcha.module b/captcha.module
index f331169..2aa3a27 100755
--- a/captcha.module
+++ b/captcha.module
@@ -102,156 +102,6 @@ function captcha_cron() {
     ->execute();
 }
 
-
-/**
- * Implements hook_element_info().
- */
-function captcha_element_info() {
-  // Define the CAPTCHA form element with default properties.
-  $captcha_element = [
-    '#input' => TRUE,
-    '#process' => ['captcha_element_process'],
-    // The type of challenge: e.g. 'default', 'captcha/Math', etc.
-    '#captcha_type' => 'default',
-    '#default_value' => '',
-    // CAPTCHA in admin mode: presolve the CAPTCHA and always show
-    // it (despite previous successful responses).
-    '#captcha_admin_mode' => FALSE,
-    // The default CAPTCHA validation function.
-    // TODO: should this be a single string or an array of strings?
-    '#captcha_validate' => 'captcha_validate_strict_equality',
-  ];
-  // Override the default CAPTCHA validation function for case
-  // insensitive validation.
-  // TODO: shouldn't this be done somewhere else, e.g. in form_alter?
-  if (CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE == \Drupal::config('captcha.settings')->get('default_validation')) {
-    $captcha_element['#captcha_validate'] = 'captcha_validate_case_insensitive_equality';
-  }
-
-  return ['captcha' => $captcha_element];
-}
-
-/**
- * Process callback for CAPTCHA form element.
- */
-function captcha_element_process(array $element, FormStateInterface $form_state, array $complete_form) {
-  // Add captcha.inc file
-  module_load_include('inc', 'captcha');
-
-  // Add Javascript for general CAPTCHA functionality.
-  $element['#attached']['library'][] = 'captcha/base';
-
-  // Get the form ID of the form we are currently processing (which is not
-  // necessary the same form that is submitted (if any).
-  $this_form_id = $complete_form['form_id']['#value'];
-
-  // Get the CAPTCHA session ID.
-  // If there is a submitted form: try to retrieve and reuse the
-  // CAPTCHA session ID from the posted data.
-  list($posted_form_id, $posted_captcha_sid) = _captcha_get_posted_captcha_info($element, $form_state, $this_form_id);
-  if ($this_form_id == $posted_form_id && isset($posted_captcha_sid)) {
-    $captcha_sid = $posted_captcha_sid;
-  }
-  else {
-    // Generate a new CAPTCHA session if we could not reuse one from a posted form.
-    $captcha_sid = _captcha_generate_captcha_session($this_form_id, CAPTCHA_STATUS_UNSOLVED);
-  }
-
-  // Store CAPTCHA session ID as hidden field.
-  // Strictly speaking, it is not necessary to send the CAPTCHA session id
-  // with the form, as the one time CAPTCHA token (see lower) is enough.
-  // However, we still send it along, because it can help debugging
-  // problems on live sites with only access to the markup.
-  $element['captcha_sid'] = [
-    '#type' => 'hidden',
-    '#value' => $captcha_sid,
-  ];
-
-  // Additional one time CAPTCHA token: store in database and send with form.
-  $captcha_token = md5(mt_rand());
-  db_update('captcha_sessions')
-    ->fields(['token' => $captcha_token])
-    ->condition('csid', $captcha_sid)
-    ->execute();
-  $element['captcha_token'] = [
-    '#type' => 'hidden',
-    '#value' => $captcha_token,
-  ];
-
-  // Get implementing module and challenge for CAPTCHA.
-  list($captcha_type_module, $captcha_type_challenge) = _captcha_parse_captcha_type($element['#captcha_type']);
-
-  // Store CAPTCHA information for further processing in
-  // - $form_state->get('captcha_info'), which survives a form rebuild (e.g. node
-  //   preview), useful in _captcha_get_posted_captcha_info().
-  // - $element['#captcha_info'], for post processing functions that do not
-  //   receive a $form_state argument (e.g. the pre_render callback).
-  $form_state->set('captcha_info', [
-    'this_form_id' => $this_form_id,
-    'posted_form_id' => $posted_form_id,
-    'captcha_sid' => $captcha_sid,
-    'module' => $captcha_type_module,
-    'captcha_type' => $captcha_type_challenge,
-  ]);
-  $element['#captcha_info'] = [
-    'form_id' => $this_form_id,
-    'captcha_sid' => $captcha_sid,
-  ];
-
-  if (_captcha_required_for_user($captcha_sid, $this_form_id) || $element['#captcha_admin_mode']) {
-    // Generate a CAPTCHA and its solution
-    // (note that the CAPTCHA session ID is given as third argument).
-    $captcha = \Drupal::moduleHandler()->invoke($captcha_type_module, 'captcha', ['generate', $captcha_type_challenge, $captcha_sid]);
-
-    // @todo Isn't this moment a bit late to figure out that we don't need CAPTCHA?
-    if(!isset($captcha)) {
-      return $element;
-    }
-
-    if (!isset($captcha['form']) || !isset($captcha['solution'])) {
-      // The selected module did not return what we expected: log about it and quit.
-      \Drupal::logger('CAPTCHA')->error(
-        'CAPTCHA problem: unexpected result from hook_captcha() of module %module when trying to retrieve challenge type %type for form %form_id.',
-        ['%type' => $captcha_type_challenge, '%module' => $captcha_type_module, '%form_id' => $this_form_id]
-      );
-
-      return $element;
-    }
-    // Add form elements from challenge as children to CAPTCHA form element.
-    $element['captcha_widgets'] = $captcha['form'];
-
-    // Add a validation callback for the CAPTCHA form element
-    // (when not in admin mode).
-    if (!$element['#captcha_admin_mode']) {
-      $element['#element_validate'] = ['captcha_validate'];
-    }
-
-    // Set a custom CAPTCHA validate function if requested.
-    if (isset($captcha['captcha_validate'])) {
-      $element['#captcha_validate'] = $captcha['captcha_validate'];
-    }
-
-    // Set the theme function.
-    $element['#theme'] = 'captcha';
-
-    // Add pre_render callback for additional CAPTCHA processing.
-    if (!isset($element['#pre_render'])) {
-      $element['#pre_render'] = [];
-    }
-    $element['#pre_render'][] = 'captcha_pre_render_process';
-
-    // Store the solution in the #captcha_info array.
-    $element['#captcha_info']['solution'] = $captcha['solution'];
-
-    // Make sure we can use a top level form value
-    // $form_state->getValue('captcha_response'), even if the form has #tree=true.
-    $element['#tree'] = FALSE;
-  }
-
-  return $element;
-}
-
-
 /**
  * Theme function for a CAPTCHA element.
  *
diff --git a/src/Element/Captcha.php b/src/Element/Captcha.php
new file mode 100644
index 0000000..83179e1
--- /dev/null
+++ b/src/Element/Captcha.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\captcha\Element\Captcha.
+ */
+
+namespace Drupal\captcha\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\FormElement;
+
+/**
+ * Defines the CAPTCHA form element with default properties.
+ *
+ * @FormElement("captcha")
+ */
+class Captcha extends FormElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $captcha_element = [
+      '#input' => TRUE,
+      '#process' => [[static::class, 'processCaptchaElement']],
+      // The type of challenge: e.g. 'default', 'captcha/Math', etc.
+      '#captcha_type' => 'default',
+      '#default_value' => '',
+      // CAPTCHA in admin mode: presolve the CAPTCHA and always show
+      // it (despite previous successful responses).
+      '#captcha_admin_mode' => FALSE,
+      // The default CAPTCHA validation function.
+      // TODO: should this be a single string or an array of strings?
+      '#captcha_validate' => 'captcha_validate_strict_equality',
+    ];
+    // Override the default CAPTCHA validation function for case
+    // insensitive validation.
+    // TODO: shouldn't this be done somewhere else, e.g. in form_alter?
+    if (CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE == \Drupal::config('captcha.settings')->get('default_validation')) {
+      $captcha_element['#captcha_validate'] = 'captcha_validate_case_insensitive_equality';
+    }
+    return $captcha_element;
+  }
+
+  /**
+   * Process callback for CAPTCHA form element.
+   */
+  public static function processCaptchaElement(&$element, FormStateInterface $form_state, &$complete_form) {
+    // Add captcha.inc file
+    module_load_include('inc', 'captcha');
+
+    // Add Javascript for general CAPTCHA functionality.
+    $element['#attached']['library'][] = 'captcha/base';
+
+    // Get the form ID of the form we are currently processing (which is not
+    // necessary the same form that is submitted (if any).
+    $this_form_id = $complete_form['form_id']['#value'];
+
+    // Get the CAPTCHA session ID.
+    // If there is a submitted form: try to retrieve and reuse the
+    // CAPTCHA session ID from the posted data.
+    list($posted_form_id, $posted_captcha_sid) = _captcha_get_posted_captcha_info($element, $form_state, $this_form_id);
+    if ($this_form_id == $posted_form_id && isset($posted_captcha_sid)) {
+      $captcha_sid = $posted_captcha_sid;
+    }
+    else {
+      // Generate a new CAPTCHA session if we could not reuse one from a posted form.
+      $captcha_sid = _captcha_generate_captcha_session($this_form_id, CAPTCHA_STATUS_UNSOLVED);
+    }
+
+    // Store CAPTCHA session ID as hidden field.
+    // Strictly speaking, it is not necessary to send the CAPTCHA session id
+    // with the form, as the one time CAPTCHA token (see lower) is enough.
+    // However, we still send it along, because it can help debugging
+    // problems on live sites with only access to the markup.
+    $element['captcha_sid'] = [
+      '#type' => 'hidden',
+      '#value' => $captcha_sid,
+    ];
+
+    // Additional one time CAPTCHA token: store in database and send with form.
+    $captcha_token = md5(mt_rand());
+    db_update('captcha_sessions')
+      ->fields(['token' => $captcha_token])
+      ->condition('csid', $captcha_sid)
+      ->execute();
+    $element['captcha_token'] = [
+      '#type' => 'hidden',
+      '#value' => $captcha_token,
+    ];
+
+    // Get implementing module and challenge for CAPTCHA.
+    list($captcha_type_module, $captcha_type_challenge) = _captcha_parse_captcha_type($element['#captcha_type']);
+
+    // Store CAPTCHA information for further processing in
+    // - $form_state->get('captcha_info'), which survives a form rebuild (e.g. node
+    //   preview), useful in _captcha_get_posted_captcha_info().
+    // - $element['#captcha_info'], for post processing functions that do not
+    //   receive a $form_state argument (e.g. the pre_render callback).
+    $form_state->set('captcha_info', [
+      'this_form_id' => $this_form_id,
+      'posted_form_id' => $posted_form_id,
+      'captcha_sid' => $captcha_sid,
+      'module' => $captcha_type_module,
+      'captcha_type' => $captcha_type_challenge,
+    ]);
+    $element['#captcha_info'] = [
+      'form_id' => $this_form_id,
+      'captcha_sid' => $captcha_sid,
+    ];
+
+    if (_captcha_required_for_user($captcha_sid, $this_form_id) || $element['#captcha_admin_mode']) {
+      // Generate a CAPTCHA and its solution
+      // (note that the CAPTCHA session ID is given as third argument).
+      $captcha = \Drupal::moduleHandler()->invoke($captcha_type_module, 'captcha', ['generate', $captcha_type_challenge, $captcha_sid]);
+
+      // @todo Isn't this moment a bit late to figure out that we don't need CAPTCHA?
+      if(!isset($captcha)) {
+        return $element;
+      }
+
+      if (!isset($captcha['form']) || !isset($captcha['solution'])) {
+        // The selected module did not return what we expected: log about it and quit.
+        \Drupal::logger('CAPTCHA')->error(
+          'CAPTCHA problem: unexpected result from hook_captcha() of module %module when trying to retrieve challenge type %type for form %form_id.',
+          ['%type' => $captcha_type_challenge, '%module' => $captcha_type_module, '%form_id' => $this_form_id]
+        );
+
+        return $element;
+      }
+      // Add form elements from challenge as children to CAPTCHA form element.
+      $element['captcha_widgets'] = $captcha['form'];
+
+      // Add a validation callback for the CAPTCHA form element
+      // (when not in admin mode).
+      if (!$element['#captcha_admin_mode']) {
+        $element['#element_validate'] = ['captcha_validate'];
+      }
+
+      // Set a custom CAPTCHA validate function if requested.
+      if (isset($captcha['captcha_validate'])) {
+        $element['#captcha_validate'] = $captcha['captcha_validate'];
+      }
+
+      // Set the theme function.
+      $element['#theme'] = 'captcha';
+
+      // Add pre_render callback for additional CAPTCHA processing.
+      if (!isset($element['#pre_render'])) {
+        $element['#pre_render'] = [];
+      }
+      $element['#pre_render'][] = 'captcha_pre_render_process';
+
+      // Store the solution in the #captcha_info array.
+      $element['#captcha_info']['solution'] = $captcha['solution'];
+
+      // Make sure we can use a top level form value
+      // $form_state->getValue('captcha_response'), even if the form has #tree=true.
+      $element['#tree'] = FALSE;
+    }
+
+    return $element;
+  }
+
+}
diff --git a/src/Tests/CaptchaCacheTestCase.php b/src/Tests/CaptchaCacheTestCase.php
index dd5cea7..b8e2ad6 100644
--- a/src/Tests/CaptchaCacheTestCase.php
+++ b/src/Tests/CaptchaCacheTestCase.php
@@ -35,6 +35,7 @@ class CaptchaCacheTestCase extends CaptchaBaseWebTestCase {
    * Test the cache tags.
    */
   public function testCacheTags() {
+    global $base_path;
     // Check caching without captcha as anonymous user.
     $this->drupalGet('');
     $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS');
@@ -67,11 +68,12 @@ class CaptchaCacheTestCase extends CaptchaBaseWebTestCase {
     // Check that we get a new image when vising the page again.
     $this->drupalGet('');
     $this->assertNotEqual($image_path, (string) $this->xpath('//div[@class="details-wrapper"]/img/@src')[0]);
-    // Check image caching.
-    $this->drupalGet($image_path);
+    // Check image caching, remove the base path since drupalGet() expects the
+    // internal path.
+    $this->drupalGet(substr($image_path, strlen($base_path)));
     $this->assertResponse(200);
     // Request image twice to make sure no errors happen (due to page caching).
-    $this->drupalGet($image_path);
+    $this->drupalGet(substr($image_path, strlen($base_path)));
     $this->assertResponse(200);
   }
 
