diff --git a/css/yamlform.element.options.css b/css/yamlform.element.options.css
new file mode 100644
index 0000000..48e70a7
--- /dev/null
+++ b/css/yamlform.element.options.css
@@ -0,0 +1 @@
+/* TODO */
\ No newline at end of file
diff --git a/src/Element/YamlFormElementOptions.php b/src/Element/YamlFormElementOptions.php
index 27898ee..d445273 100755
--- a/src/Element/YamlFormElementOptions.php
+++ b/src/Element/YamlFormElementOptions.php
@@ -94,7 +94,7 @@ class YamlFormElementOptions extends FormElement {
     // Custom options.
     $element['custom']['#title'] = $element['#title'];
     $element['custom']['#title_display'] = 'invisible';
-    $element['custom']['#type'] = 'yamlform_codemirror';
+    $element['custom']['#type'] = 'yamlform_options';
     $element['custom']['#mode'] = 'yaml';
     $element['custom']['#states'] = [
       'visible' => [
diff --git a/src/Element/YamlFormOptions.php b/src/Element/YamlFormOptions.php
new file mode 100644
index 0000000..8ba566b
--- /dev/null
+++ b/src/Element/YamlFormOptions.php
@@ -0,0 +1,437 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\yamlform\Element\YamlFormOptions.
+ */
+
+namespace Drupal\yamlform\Element;
+
+use Drupal\Core\Render\Element\FormElement;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\yamlform\Utility\YamlFormElementHelper;
+use Drupal\Component\Serialization\Yaml;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+
+/**
+ * Provides a form element to assist in creation of options.
+ *
+ * This provides a nicer interface for non-technical users to add values and
+ * labels for options, possible within option groups.
+ *
+ * @FormElement("yamlform_options")
+ */
+class YamlFormOptions extends FormElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return [
+      '#input' => TRUE,
+      '#size' => 60,
+      '#process' => [
+        [$class, 'processOptions'],
+      ],
+      '#theme_wrappers' => ['form_element'],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+    if ($input === FALSE) {
+      return $element;
+    }
+    return NULL;
+  }
+
+  /**
+   * Expand an email confirm field into two HTML5 email elements.
+   */
+  public static function processOptions(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element['#tree'] = TRUE;
+
+    $element['options_container'] = [
+      '#type' => 'fieldset',
+      '#title' => t('Options'),
+    ];
+
+    // Get the current options.
+    $options = [];
+    $options_yaml = $element['#value']['#default_value'];
+    if ($options_yaml) {
+      $options = Yaml::decode($options_yaml);
+    }
+
+    // Build the table and add the Tabledrag.
+    $table = self::buildTable($element, $options);
+    $table = self::addTableDrag($table);
+    $element['options_container']['options'] = $table;
+
+    // Add additional CSS for this table to assist with a better UI.
+    $element['options_container']['options']['#attached']['library'][] = 'taxonomy/drupal.taxonomy';
+
+    // Add options.
+    $element['options_container']['add_options_action'] = [
+      '#type' => 'fieldgroup',
+      '#attributes' => ['class' => ['options-action-add-options']],
+    ];
+
+    // Tools to add new options or option groups.
+    $element['options_container']['add_options_action']['add_options_quantity'] = [
+      '#type' => 'number',
+      '#min' => 1,
+      '#max' => 1000,
+      '#default_value' => 1,
+      '#title' => t('Quantity to add'),
+    ];
+    $element['options_container']['add_options_action']['add_options_type'] = [
+      '#type' => 'select',
+      '#options' => [
+        'option' => t('Option(s)'),
+        'optgroup' => t('Option Group(s)'),
+      ],
+      '#title' => t('Type to add'),
+    ];
+    $element['options_container']['add_options_action']['add_options'] = [
+      '#type' => 'submit',
+      '#value' => t('Add options'),
+      '#ajax' => [
+        'callback' => '\Drupal\yamlform\Element\YamlFormOptions::addOptionsCallback',
+        'wrapper' => 'edit-properties-options-custom-options-container-options-table',
+        'event' => 'click',
+        'method' => 'append',
+        'effect' => 'slide',
+      ],
+    ];
+
+    // Unset element title to prevent duplicate titles.
+    unset($element['#title']);
+
+    // TODO default styling for the interface to add new options.
+    $element['#attached']['library'][] = 'yamlform/yamlform.element.options';
+
+    // Wrap this $element in a <div> that handle #states.
+    YamlFormElementHelper::fixStates($element);
+
+    return $element;
+  }
+
+  /**
+   * Build the table render array.
+   *
+   * This is used both by the process callback and ajax callback.
+   *
+   * @param array $element
+   *   The field element.
+   * @param array $options
+   *   The decoded yaml options.
+   *
+   * @return array
+   *   Table render array.
+   */
+  protected static function buildTable($element, $options) {
+    $table = [
+      '#type' => 'table',
+      '#header' => [t('Option'), '', t('Weight'), t('Operations')],
+      '#empty' => t('No options available.'),
+      '#attributes' => [
+        'id' => 'options-table',
+      ],
+    ];
+    $key = 0;
+    if ($options) {
+      foreach ($options as $value => $label) {
+        $key = self::addTableRow($element, $table, $key, $value, $label);
+      }
+    }
+    else {
+      $value = '';
+      $label = '';
+      $key = self::addTableRow($element, $table, $key, $value, $label);
+    }
+
+    return $table;
+  }
+
+  /**
+   * Add the table row to the table render array.
+   *
+   * @param array $table
+   *   The table render array.
+   *
+   * @return array
+   *   Table render array.
+   */
+  protected static function addTableDrag($table) {
+    $table['#tabledrag'][] = array(
+      'action' => 'match',
+      'relationship' => 'parent',
+      'group' => 'option-parent',
+      'subgroup' => 'option-parent',
+      'source' => 'option-id',
+      'hidden' => FALSE,
+    );
+    $table['#tabledrag'][] = array(
+      'action' => 'depth',
+      'relationship' => 'group',
+      'group' => 'option-depth',
+      'hidden' => FALSE,
+      'limit' => 1,
+    );
+    $table['#tabledrag'][] = array(
+      'action' => 'order',
+      'relationship' => 'sibling',
+      'group' => 'option-weight',
+    );
+    return $table;
+  }
+
+  /**
+   * Add the table row to the table render array.
+   *
+   * @param array $element
+   *   The field element.
+   * @param array $table
+   *   The table render array.
+   * @param int $key
+   *   The current row key.
+   * @param string $value
+   *   The option value or option group.
+   * @param mixed $label
+   *   The option label or an array of values and labels if an option group.
+   * @param int $depth
+   *   The depth of either 0 or 1.
+   * @param int $parent_key
+   *   The key of the parent if any.
+   */
+  protected static function addTableRow($element, &$table, $key, $value = '', $label = '', $depth = 0, $parent_key = 0) {
+
+    $indentation = array();
+    if ($depth > 0) {
+      $indentation = array(
+        '#theme' => 'indentation',
+        '#size' => $depth,
+      );
+    }
+
+    if (is_array($label)) {
+
+      // Optgroup input field.
+      $table[$key]['optgroup'] = array(
+        '#prefix' => !empty($indentation) ? drupal_render($indentation) : '',
+        '#type' => 'textfield',
+        '#title' => t('Option group'),
+        '#default_value' => $value,
+      );
+
+      // Tabledrag does not work with multiple fields in one column. We must
+      // instead allow for key and value in separate columns for an option
+      // and match the number of columns for option group by adding this
+      // empty field.
+      $table[$key]['optgroup_empty'] = array(
+        '#type' => 'markup',
+      );
+
+    }
+    else {
+
+      // Option input field value.
+      $table[$key]['value'] = array(
+        '#prefix' => !empty($indentation) ? drupal_render($indentation) : '',
+        '#type' => 'textfield',
+        '#title' => t('Value'),
+        '#default_value' => $value,
+      );
+
+      // Option input field label.
+      $table[$key]['label'] = array(
+        '#prefix' => !empty($indentation) ? drupal_render($indentation) : '',
+        '#type' => 'textfield',
+        '#title' => t('Label'),
+        '#default_value' => $label,
+      );
+
+    }
+
+    // Tabledrag settings storage.
+    $table[$key]['option']['key'] = array(
+      '#type' => 'hidden',
+      '#value' => $key,
+      '#attributes' => array(
+        'class' => array('option-id'),
+      ),
+    );
+    $table[$key]['option']['parent'] = array(
+      '#type' => 'hidden',
+      '#default_value' => $parent_key,
+      '#attributes' => array(
+        'class' => array('option-parent'),
+      ),
+    );
+    $table[$key]['option']['depth'] = array(
+      '#type' => 'hidden',
+      '#default_value' => $depth,
+      '#attributes' => array(
+        'class' => array('option-depth'),
+      ),
+    );
+    $table[$key]['weight'] = array(
+      '#type' => 'weight',
+      '#delta' => 0,
+      '#title' => t('Weight for added option'),
+      '#title_display' => 'invisible',
+      '#default_value' => 0,
+      '#attributes' => array(
+        'class' => array('option-weight'),
+      ),
+    );
+
+    // Activate tabledrag for this row.
+    $table[$key]['#attributes']['class'] = [];
+    $table[$key]['#attributes']['class'][] = 'draggable';
+
+    // Store current key in case we have sub options before incrementing key.
+    $parent_key = $key;
+    $key++;
+
+    // If we have options within optgroup, use depth.
+    if (is_array($label)) {
+      foreach ($label as $subvalue => $sublabel) {
+        $key = self::addTableRow($element, $table, $key, $subvalue, $sublabel, ($depth + 1), $parent_key);
+      }
+    }
+
+    return $key;
+  }
+
+  /**
+   * Ajax callback for adding one or more option rows.
+   *
+   * @param array $element
+   *   Drupal core FormElement.
+   * @param FormStateInterface $form_state
+   *   Drupal core FormStateInterface.
+   * @param array $complete_form
+   *   Drupal core Form.
+   *
+   * @return Response
+   *   Response object.
+   */
+  public static function addOptionsCallback($element, FormStateInterface $form_state, $complete_form) {
+
+    $rendered = '';
+    if ($key = self::getTriggeringElementFormKey($form_state)) {
+
+      // Break apart the values for ease of access.
+      $user_input = $form_state->getUserInput();
+      $all_values = $user_input['properties']['options'];
+      $option_values = $all_values[$key]['options_container'];
+      $quantity = $option_values['add_options_action']['add_options_quantity'];
+      $type = $option_values['add_options_action']['add_options_type'];
+      $options = self::optionsInputToDecodedYaml($option_values['options']);
+
+      // Create the table.
+      $table = self::buildTable($element, $options);
+      $key = end(array_keys($table));
+
+      // Add the desired additional inputs.
+      if ($quantity) {
+        for ($i = 0; $i < $quantity; $i++) {
+          $value = '';
+          $label = ($type == 'optgroup' ? [] : '');
+          $key = self::addTableRow($element, $table, $key, $value, $label);
+        }
+      }
+
+      // Add the Tabledrag functionality.
+      $table = self::addTableDrag($table);
+
+      // Render the table.
+      // TODO determine how to render without losing the default values.
+      $rendered = drupal_render($table);
+
+    }
+
+    // TODO need a solution for ajax response.
+    // This returns the ajax response BUT Drupal 8 core auto wraps that in a
+    // div, breaking the table layout: https://www.drupal.org/node/736066
+    $response = new AjaxResponse();
+    $append = new ReplaceCommand(
+      '#options-table',
+      $rendered
+    );
+    $response->addCommand($append);
+    return $response;
+  }
+
+  /**
+   * Get the key of the element triggering the ajax callback.
+   *
+   * @param FormStateInterface $form_state
+   *   Drupal FormStateInterface.
+   *
+   * @return string/boolean
+   *   The form key of the element triggering the ajax callback or false.
+   */
+  protected static function getTriggeringElementFormKey(FormStateInterface $form_state) {
+    $parents = $form_state->getTriggeringElement()['#array_parents'];
+    $options_container_key = array_search('options_container', $parents);
+    if ($options_container_key) {
+      $triggering_element_form_key = $options_container_key - 1;
+      return $parents[$triggering_element_form_key];
+    }
+    return FALSE;
+  }
+
+  /**
+   * Provide a YAML Form compatible array for consistent table generation.
+   *
+   * @param array $options
+   *   Options submitted within the form state.
+   *
+   * @return array
+   *   Options formatted consistently with the array we get by decoding the
+   *   YAML form.
+   */
+  protected static function optionsInputToDecodedYaml($options) {
+    $yaml_compatible = [];
+    if ($options) {
+
+      // Store the last optgroup as each row is only aware if it has a depth
+      // but not the parent optgroup.
+      $last_optgroup = FALSE;
+
+      foreach ($options as $key => $data) {
+        if (isset($data['optgroup'])) {
+
+          // Optgroup.
+          $yaml_compatible[$data['optgroup']] = [];
+          $last_optgroup = $data['optgroup'];
+
+        }
+        else {
+          if ($data['option']['depth'] && $last_optgroup) {
+
+            // Label and value within optgroup.
+            $yaml_compatible[$last_optgroup][$data['value']] = $data['label'];
+
+          }
+          else {
+
+            // Non nested label and value.
+            $yaml_compatible[$data['value']] = $data['label'];
+            $last_optgroup = FALSE;
+
+          }
+        }
+      }
+    }
+    return $yaml_compatible;
+  }
+
+}
