diff --git a/README.txt b/README.txt
index 921c0d3..93788c0 100644
--- a/README.txt
+++ b/README.txt
@@ -21,7 +21,7 @@ INSTALLATION
 ------------
 
 Checklist API is installed in the usual way. See
-http://drupal.org/documentation/install/modules-themes/modules-7.
+http://drupal.org/documentation/install/modules-themes/modules-8.
 
 
 IMPLEMENTATION
@@ -30,8 +30,7 @@ IMPLEMENTATION
 Checklists are declared as multidimensional arrays using
 hook_checklistapi_checklist_info(). They can be altered using
 hook_checklistapi_checklist_info_alter(). Checklist API handles creation of menu
-items and permissions. Progress details are saved in one Drupal variable per
-checklist. (Note: it is the responsibility of implementing modules to remove
-their own variables on hook_uninstall().)
+items and permissions. Progress details are saved in one config file per
+checklist.
 
 See checklistapi.api.php for more details.
diff --git a/checklistapi.admin.inc b/checklistapi.admin.inc
deleted file mode 100644
index 382e566..0000000
--- a/checklistapi.admin.inc
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-/**
- * @file
- * Admin page callback file for the Checklist API module.
- */
-
-/**
- * Page callback: Form constructor for the report form.
- *
- * @see checklistapi_menu()
- *
- * @ingroup forms
- */
-function checklistapi_report_form() {
-  // Define table header.
-  $header = array(
-    t('Checklist'),
-    t('Progress'),
-    t('Last updated'),
-    t('Last updated by'),
-    t('Operations'),
-  );
-
-  // Build table rows.
-  $rows = array();
-  $definitions = checklistapi_get_checklist_info();
-  foreach ($definitions as $id => $definition) {
-    $checklist = checklistapi_checklist_load($id);
-    $row = array();
-    $row[] = array(
-      'data' => ($checklist->userHasAccess()) ? l($checklist->title, $checklist->path) : drupal_placeholder($checklist->title),
-      'title' => (!empty($checklist->description)) ? $checklist->description : '',
-    );
-    $row[] = t('@completed of @total (@percent%)', array(
-      '@completed' => $checklist->getNumberCompleted(),
-      '@total' => $checklist->getNumberOfItems(),
-      '@percent' => round($checklist->getPercentComplete()),
-    ));
-    $row[] = $checklist->getLastUpdatedDate();
-    $row[] = $checklist->getLastUpdatedUser();
-    $row[] = ($checklist->userHasAccess('edit') && $checklist->hasSavedProgress()) ? l(t('clear saved progress'), $checklist->path . '/clear', array(
-      'query' => array('destination' => 'admin/reports/checklistapi'),
-    )) : '';
-    $rows[] = $row;
-  }
-
-  // Compile table.
-  $table = array(
-    'header' => $header,
-    'rows' => $rows,
-    'empty' => t('No checklists available.'),
-  );
-
-  return theme('table', $table);
-}
diff --git a/checklistapi.api.php b/checklistapi.api.php
index bc02a71..951d4e4 100644
--- a/checklistapi.api.php
+++ b/checklistapi.api.php
@@ -27,7 +27,7 @@
  *   - #help: (optional) User help to be displayed in the "System help" block
  *     via hook_help().
  *   - #menu_name: (optional) The machine name of a menu to place the checklist
- *     into (e.g. "main-menu" or "navigation"). If this is omitted, Drupal will
+ *     into (e.g., "main-menu" or "navigation"). If this is omitted, Drupal will
  *     try to infer the correct menu placement from the specified path.
  *   - #weight: (optional) A floating point number used to sort the list of
  *     checklists before being output. Lower numbers appear before higher
@@ -50,8 +50,8 @@
  *       - #default_value: (optional) The default checked state of the
  *         item--TRUE for checked or FALSE for unchecked. Defaults to FALSE.
  *         This is useful for automatically checking items that can be
- *         programmatically tested (e.g. a module is installed or a variable has
- *         a certain value).
+ *         programmatically tested (e.g., a module is installed or a
+ *         configuration setting has a certain value).
  *       - #weight: (optional) A floating point number used to sort the list of
  *         items before being output. Lower numbers appear before higher
  *         numbers.
diff --git a/checklistapi.css b/checklistapi.css
index b7cc6ea..a954ede 100644
--- a/checklistapi.css
+++ b/checklistapi.css
@@ -1,4 +1,3 @@
-
 #checklistapi-checklist-form div.description p {
   margin: .5em 0;
 }
@@ -19,7 +18,11 @@
   font-weight: normal;
   margin-bottom: 0.5em;
 }
-#checklistapi-checklist-form .progress .bar,
-#checklistapi-checklist-form .progress .filled {
+#checklistapi-checklist-form .progress__bar {
+  animation-name: none;
   background-image: none;
 }
+#checklistapi-checklist-form .compact-link {
+  clear: both;
+  padding-top: 1em;
+}
diff --git a/checklistapi.info b/checklistapi.info
deleted file mode 100644
index a4c534b..0000000
--- a/checklistapi.info
+++ /dev/null
@@ -1,7 +0,0 @@
-name = Checklist API
-description = Provides an API for creating fillable, persistent checklists.
-core = 7.x
-package = Other
-files[] = lib/Drupal/checklistapi/ChecklistapiChecklist.php
-files[] = tests/checklistapi.test
-configure = admin/reports/checklistapi
diff --git a/checklistapi.info.yml b/checklistapi.info.yml
new file mode 100644
index 0000000..8f3c04b
--- /dev/null
+++ b/checklistapi.info.yml
@@ -0,0 +1,7 @@
+name: Checklist API
+type: module
+description: Provides an API for creating fillable, persistent checklists.
+version: VERSION
+package: Other
+core: 8.x
+configure: checklistapi.report
diff --git a/checklistapi.install b/checklistapi.install
deleted file mode 100644
index a42f1b0..0000000
--- a/checklistapi.install
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Install, update and uninstall functions for the Checklist API module.
- */
-
-/**
- * Implements hook_uninstall().
- */
-function checklistapi_uninstall() {
-  // Delete all Checklist API persistent variables.
-  db_delete('variable')
-    ->condition('name', db_like('checklistapi_') . '%', 'LIKE')
-    ->execute();
-  cache_clear_all('variables', 'cache_bootstrap');
-}
diff --git a/checklistapi.js b/checklistapi.js
index c4c3707..946a01d 100644
--- a/checklistapi.js
+++ b/checklistapi.js
@@ -2,12 +2,34 @@
   "use strict";
 
   /**
+   * Updates the progress bar as checkboxes are changed.
+   */
+  Drupal.behaviors.checklistapiUpdateProgressBar = {
+    attach: function (context) {
+      var total_items = $(':checkbox.checklistapi-item', context).size(),
+        progress_bar = $('#checklistapi-checklist-form .progress__bar', context),
+        progress_percentage = $('#checklistapi-checklist-form .progress__percentage', context);
+      $(':checkbox.checklistapi-item', context).change(function () {
+        var num_items_checked = $(':checkbox.checklistapi-item:checked', context).size(),
+          percent_complete = Math.round(num_items_checked / total_items * 100),
+          args = {};
+        progress_bar.css('width', percent_complete + '%');
+        args['@complete'] = num_items_checked;
+        args['@total'] = total_items;
+        args['@percent'] = percent_complete;
+        progress_percentage.html(Drupal.t('@complete of @total (@percent%)', args));
+      });
+    }
+  };
+
+  /**
    * Provides the summary information for the checklist form vertical tabs.
    */
   Drupal.behaviors.checklistapiFieldsetSummaries = {
     attach: function (context) {
-      $('#checklistapi-checklist-form .vertical-tabs-panes > fieldset', context).drupalSetSummary(function (context) {
-        var total = $(':checkbox.checklistapi-item', context).size(), args = {};
+      $('#checklistapi-checklist-form .vertical-tabs-panes > details', context).drupalSetSummary(function (context) {
+        var total = $(':checkbox.checklistapi-item', context).size(),
+          args = {};
         if (total) {
           args['@complete'] = $(':checkbox.checklistapi-item:checked', context).size();
           args['@total'] = total;
@@ -53,10 +75,10 @@
           return Drupal.t('Your changes will be lost if you leave the page without saving.');
         }
       });
-      $('#checklistapi-checklist-form').submit(function() {
+      $('#checklistapi-checklist-form').submit(function () {
         $(window).unbind('beforeunload');
       });
-      $('#checklistapi-checklist-form .clear-saved-progress').click(function() {
+      $('#checklistapi-checklist-form .clear-saved-progress').click(function () {
         $(window).unbind('beforeunload');
       });
     }
diff --git a/checklistapi.menu_links.yml b/checklistapi.menu_links.yml
new file mode 100644
index 0000000..09ec540
--- /dev/null
+++ b/checklistapi.menu_links.yml
@@ -0,0 +1,5 @@
+checklistapi.report:
+  title: Checklists
+  description: Get an overview of your installed checklists with progress details.
+  route_name: checklistapi.report
+  parent: system.admin_reports
diff --git a/checklistapi.module b/checklistapi.module
index c229177..7340158 100644
--- a/checklistapi.module
+++ b/checklistapi.module
@@ -8,6 +8,10 @@
  * completion times and users.
  */
 
+use Drupal\checklistapi\ChecklistapiChecklist;
+use Drupal\checklistapi\ChecklistapiException;
+use Drupal\Core\Render\Element;
+
 /**
  * Access callback: Checks the current user's access to a given checklist.
  *
@@ -20,17 +24,19 @@
  * @return bool
  *   Returns TRUE if the current user has access to perform a given operation on
  *   the specified checklist, or FALSE if not.
+ *
+ * @throws ChecklistapiException
+ *   Throws an exception if an unsupported operation is supplied.
  */
 function checklistapi_checklist_access($id, $operation = 'any') {
   $all_operations = array('view', 'edit', 'any');
   if (!in_array($operation, $all_operations)) {
-    throw new Exception(t('No such operation "@operation"', array(
-      '@operation' => $operation,
-    )));
+    throw new \InvalidArgumentException(sprintf('No such operation "%s"', $operation));
   }
 
-  $access['view'] = user_access('view any checklistapi checklist') || user_access('view ' . $id . ' checklistapi checklist');
-  $access['edit'] = user_access('edit any checklistapi checklist') || user_access('edit ' . $id . ' checklistapi checklist');
+  $current_user = \Drupal::currentUser();
+  $access['view'] = $current_user->hasPermission('view any checklistapi checklist') || $current_user->hasPermission("view {$id} checklistapi checklist");
+  $access['edit'] = $current_user->hasPermission('edit any checklistapi checklist') || $current_user->hasPermission("edit {$id} checklistapi checklist");
   $access['any'] = $access['view'] || $access['edit'];
   return $access[$operation];
 }
@@ -50,6 +56,22 @@ function checklistapi_checklist_load($id) {
 }
 
 /**
+ * Determines whether the current user is in compact mode.
+ *
+ * Compact mode shows checklist forms with less description text.
+ *
+ * Whether the user is in compact mode is determined by a cookie, which is set
+ * for the user by ChecklistapiController::setCompactMode(). If the user does
+ * not have the cookie, the setting defaults to off.
+ *
+ * @return bool
+ *   TRUE when in compact mode, or FALSE when in expanded mode.
+ */
+function checklistapi_compact_mode() {
+  return isset($_COOKIE['Drupal_visitor_checklistapi_compact_mode']) ? (bool) $_COOKIE['Drupal_visitor_checklistapi_compact_mode'] : FALSE;
+}
+
+/**
  * Gets checklist definitions.
  *
  * @param string $id
@@ -63,10 +85,10 @@ function checklistapi_get_checklist_info($id = NULL) {
   $definitions = &drupal_static(__FUNCTION__);
   if (!isset($definitions)) {
     // Get definitions.
-    $definitions = module_invoke_all('checklistapi_checklist_info');
+    $definitions = \Drupal::moduleHandler()->invokeAll('checklistapi_checklist_info');
     $definitions = checklistapi_sort_array($definitions);
     // Let other modules alter them.
-    drupal_alter('checklistapi_checklist_info', $definitions);
+    \Drupal::moduleHandler()->alter('checklistapi_checklist_info', $definitions);
     $definitions = checklistapi_sort_array($definitions);
     // Inject checklist IDs.
     foreach ($definitions as $key => $value) {
@@ -91,77 +113,6 @@ function checklistapi_help($path, $arg) {
 }
 
 /**
- * Implements hook_init().
- */
-function checklistapi_init() {
-  // Disable page caching on all Checklist API module paths.
-  $module_paths = array_keys(checklistapi_menu());
-  if (in_array(current_path(), $module_paths)) {
-    drupal_page_is_cacheable(FALSE);
-  }
-}
-
-/**
- * Implements hook_menu().
- */
-function checklistapi_menu() {
-  $items = array();
-
-  // Checklists report.
-  $items['admin/reports/checklistapi'] = array(
-    'title' => 'Checklists',
-    'page callback' => 'checklistapi_report_form',
-    'access arguments' => array('view checklistapi checklists report'),
-    'description' => 'Get an overview of your installed checklists with progress details.',
-    'file' => 'checklistapi.admin.inc',
-  );
-
-  // Individual checklists.
-  foreach (checklistapi_get_checklist_info() as $id => $definition) {
-    if (empty($definition['#path']) || empty($definition['#title'])) {
-      continue;
-    }
-
-    // View/edit checklist.
-    $items[$definition['#path']] = array(
-      'title' => $definition['#title'],
-      'description' => (!empty($definition['#description'])) ? $definition['#description'] : '',
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array('checklistapi_checklist_form', $id),
-      'access callback' => 'checklistapi_checklist_access',
-      'access arguments' => array($id),
-      'file' => 'checklistapi.pages.inc',
-    );
-    if (!empty($definition['#menu_name'])) {
-      $items[$definition['#path']]['menu_name'] = $definition['#menu_name'];
-    }
-
-    // Clear saved progress.
-    $items[$definition['#path'] . '/clear'] = array(
-      'title' => 'Clear',
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array('checklistapi_checklist_clear_confirm', $id),
-      'access callback' => 'checklistapi_checklist_access',
-      'access arguments' => array($id, 'edit'),
-      'file' => 'checklistapi.pages.inc',
-      'type' => MENU_CALLBACK,
-    );
-
-    // Toggle compact mode.
-    $items[$definition['#path'] . '/compact'] = array(
-      'title' => 'Compact mode',
-      'page callback' => 'checklistapi_compact_page',
-      'access callback' => 'checklistapi_checklist_access',
-      'access arguments' => array($id),
-      'file' => 'checklistapi.pages.inc',
-      'type' => MENU_CALLBACK,
-    );
-  }
-
-  return $items;
-}
-
-/**
  * Implements hook_permission().
  */
 function checklistapi_permission() {
@@ -171,7 +122,7 @@ function checklistapi_permission() {
   $perms['view checklistapi checklists report'] = array(
     'title' => t(
       'View the !name report',
-      array('!name' => (user_access('view checklistapi checklists report')) ? l(t('Checklists'), 'admin/reports/checklistapi') : drupal_placeholder('Checklists'))
+      array('!name' => (\Drupal::currentUser()->hasPermission('view checklistapi checklists report')) ? l(t('Checklists'), 'admin/reports/checklistapi') : drupal_placeholder('Checklists'))
     ),
   );
   $perms['view any checklistapi checklist'] = array(
@@ -188,18 +139,14 @@ function checklistapi_permission() {
     if (empty($id)) {
       continue;
     }
-    $perms['view ' . $id . ' checklistapi checklist'] = array(
-      'title' => t(
-        'View the !name checklist',
-        array('!name' => (checklistapi_checklist_access($id)) ? l($definition['#title'], $definition['#path']) : drupal_placeholder($definition['#title']))
-      ),
+
+    $checklist_name = (checklistapi_checklist_access($id)) ? l($definition['#title'], $definition['#path']) : drupal_placeholder($definition['#title']);
+    $perms["view {$id} checklistapi checklist"] = array(
+      'title' => t('View the !name checklist', array('!name' => $checklist_name)),
       'description' => $view_checklist_perm_description,
     );
-    $perms['edit ' . $id . ' checklistapi checklist'] = array(
-      'title' => t(
-        'Edit the !name checklist',
-        array('!name' => (checklistapi_checklist_access($id)) ? l($definition['#title'], $definition['#path']) : drupal_placeholder($definition['#title']))
-      ),
+    $perms["edit {$id} checklistapi checklist"] = array(
+      'title' => t('Edit the !name checklist', array('!name' => $checklist_name)),
       'description' => $edit_checklist_perm_description,
     );
   }
@@ -220,7 +167,7 @@ function checklistapi_permission() {
  * @see checklistapi_get_checklist_info()
  */
 function checklistapi_sort_array(array $array) {
-  $child_keys = element_children($array);
+  $child_keys = Element::children($array);
 
   if (!count($child_keys)) {
     // No children to sort.
@@ -245,7 +192,7 @@ function checklistapi_sort_array(array $array) {
     $children[$key] = checklistapi_sort_array($children[$key]);
   }
   // Sort by weight.
-  uasort($children, 'element_sort');
+  uasort($children, array('Drupal\Component\Utility\SortArray', 'sortByWeightProperty'));
   // Remove incremental weight hack.
   foreach ($children as $key => $child) {
     $children[$key]['#weight'] = floor($children[$key]['#weight']);
@@ -269,9 +216,7 @@ function checklistapi_strtolowercamel($string) {
   $string = str_replace('_', ' ', $string);
   $string = ucwords($string);
   $string = str_replace(' ', '', $string);
-  // Lowercase first character. lcfirst($string) would be nicer, but let's not
-  // create a dependency on PHP 5.3 just for that.
-  $string[0] = strtolower($string[0]);
+  $string = lcfirst($string);
   return $string;
 }
 
@@ -281,7 +226,8 @@ function checklistapi_strtolowercamel($string) {
 function checklistapi_theme() {
   return array(
     'checklistapi_compact_link' => array(
-      'file' => 'checklistapi.pages.inc',
+      'template' => 'checklistapi-compact-link',
+      'variables' => array('link' => ''),
     ),
     'checklistapi_progress_bar' => array(
       'path' => drupal_get_path('module', 'checklistapi') . '/templates',
@@ -295,3 +241,31 @@ function checklistapi_theme() {
     ),
   );
 }
+
+/**
+ * Prepares variables for checklist compact link templates.
+ *
+ * Default template: checklistapi-compact-link.html.twig.
+ *
+ * @param array $variables
+ *   An empty array.
+ */
+function template_preprocess_checklistapi_compact_link(&$variables) {
+  if (checklistapi_compact_mode()) {
+    $text = t('Show item descriptions');
+    $path = current_path() . '/compact/off';
+    $title = t('Expand layout to include item descriptions.');
+  }
+  else {
+    $text = t('Hide item descriptions');
+    $path = current_path() . '/compact/on';
+    $title = t('Compress layout by hiding item descriptions.');
+  }
+
+  $variables['link'] = l($text, $path, array(
+    'attributes' => array(
+      'title' => $title,
+    ),
+    'query' => drupal_get_destination(),
+  ));
+}
diff --git a/checklistapi.pages.inc b/checklistapi.pages.inc
deleted file mode 100644
index 9b832f3..0000000
--- a/checklistapi.pages.inc
+++ /dev/null
@@ -1,258 +0,0 @@
-<?php
-
-/**
- * @file
- * Page callbacks for the Checklist API module.
- */
-
-/**
- * Page callback: Form constructor for "Clear saved progress" confirmation form.
- *
- * @param string $id
- *   The checklist ID.
- *
- * @see checklistapi_menu()
- *
- * @ingroup forms
- */
-function checklistapi_checklist_clear_confirm($form, &$form_state, $id) {
-  $checklist = checklistapi_checklist_load($id);
-  $form['#checklist'] = $checklist;
-  $question = t('Are you sure you want to clear %title saved progress?', array(
-    '%title' => $checklist->title,
-  ));
-  $description = t('All progress details will be erased. This action cannot be undone.');
-  $yes = t('Clear');
-  $no = t('Cancel');
-  return confirm_form($form, $question, $checklist->path, $description, $yes, $no);
-}
-
-/**
- * Form submission handler for checklistapi_checklist_clear_confirm().
- */
-function checklistapi_checklist_clear_confirm_submit($form, &$form_state) {
-  // If user confirmed, clear saved progress.
-  if ($form_state['values']['confirm']) {
-    $form['#checklist']->clearSavedProgress();
-  }
-
-  // Redirect back to checklist.
-  $form_state['redirect'] = $form['#checklist']->path;
-}
-
-/**
- * Page callback: Form constructor for the checklist form.
- *
- * @param string $id
- *   The checklist ID.
- *
- * @see checklistapi_checklist_form_submit()
- * @see checklistapi_menu()
- *
- * @ingroup forms
- */
-function checklistapi_checklist_form($form, &$form_state, $id) {
-  $form['#checklist'] = $checklist = checklistapi_checklist_load($id);
-
-  $form['progress_bar'] = array(
-    '#type' => 'markup',
-    '#markup' => theme('checklistapi_progress_bar', array(
-      'message' => ($checklist->hasSavedProgress()) ? t('Last updated @date by !user', array(
-        '@date' => $checklist->getLastUpdatedDate(),
-        '!user' => $checklist->getLastUpdatedUser(),
-      )) : '&nbsp;',
-      'number_complete' => $checklist->getNumberCompleted(),
-      'number_of_items' => $checklist->getNumberOfItems(),
-      'percent_complete' => round($checklist->getPercentComplete()),
-    )),
-  );
-  if (checklistapi_compact_mode()) {
-    $form['#attributes']['class'] = array('compact-mode');
-  }
-  $form['compact_mode_link'] = array(
-    '#markup' => theme('checklistapi_compact_link'),
-  );
-
-  $form['checklistapi'] = array(
-    '#attached' => array(
-      'css' => array(drupal_get_path('module', 'checklistapi') . '/checklistapi.css'),
-      'js' => array(drupal_get_path('module', 'checklistapi') . '/checklistapi.js'),
-    ),
-    '#tree' => TRUE,
-    '#type' => 'vertical_tabs',
-  );
-
-  // Loop through groups.
-  $num_autochecked_items = 0;
-  $groups = $checklist->items;
-  foreach (element_children($groups) as $group_key) {
-    $group = &$groups[$group_key];
-    $form['checklistapi'][$group_key] = array(
-      '#title' => filter_xss($group['#title']),
-      '#type' => 'fieldset',
-    );
-    if (!empty($group['#description'])) {
-      $form['checklistapi'][$group_key]['#description'] = filter_xss_admin($group['#description']);
-    }
-
-    // Loop through items.
-    foreach (element_children($group) as $item_key) {
-      $item = &$group[$item_key];
-      $saved_item = !empty($checklist->savedProgress[$item_key]) ? $checklist->savedProgress[$item_key] : 0;
-      // Build title.
-      $title = filter_xss($item['#title']);
-      if ($saved_item) {
-        // Append completion details.
-        $user = user_load($saved_item['#uid']);
-        $title .= t(
-          '<span class="completion-details"> - Completed @time by !user</a>',
-          array(
-            '@time' => format_date($saved_item['#completed'], 'short'),
-            '!user' => theme('username', array('account' => $user)),
-          )
-        );
-      }
-      // Set default value.
-      $default_value = FALSE;
-      if ($saved_item) {
-        $default_value = TRUE;
-      }
-      elseif (!empty($item['#default_value'])) {
-        if ($default_value = $item['#default_value']) {
-          $num_autochecked_items++;
-        }
-      }
-      // Get description.
-      $description = (isset($item['#description'])) ? '<p>' . filter_xss_admin($item['#description']) . '</p>' : '';
-      // Append links.
-      $links = array();
-      foreach (element_children($item) as $link_key) {
-        $link = &$item[$link_key];
-        $options = (!empty($link['#options']) && is_array($link['#options'])) ? $link['#options'] : array();
-        $links[] = l($link['#text'], $link['#path'], $options);
-      }
-      if (count($links)) {
-        $description .= '<div class="links">' . implode(' | ', $links) . '</div>';
-      }
-      // Compile the list item.
-      $form['checklistapi'][$group_key][$item_key] = array(
-        '#attributes' => array('class' => array('checklistapi-item')),
-        '#default_value' => $default_value,
-        '#description' => filter_xss_admin($description),
-        '#disabled' => !($user_has_edit_access = $checklist->userHasAccess('edit')),
-        '#title' => filter_xss_admin($title),
-        '#type' => 'checkbox',
-      );
-    }
-  }
-
-  $form['actions'] = array(
-    '#access' => $user_has_edit_access,
-    '#type' => 'actions',
-    '#weight' => 100,
-    'save' => array(
-      '#submit' => array('checklistapi_checklist_form_submit'),
-      '#type' => 'submit',
-      '#value' => t('Save'),
-    ),
-    'clear' => array(
-      '#access' => $checklist->hasSavedProgress(),
-      '#attributes' => array('class' => array('clear-saved-progress')),
-      '#href' => $checklist->path . '/clear',
-      '#title' => t('Clear saved progress'),
-      '#type' => 'link',
-    ),
-  );
-
-  // Alert the user of autochecked items. Only set the message on GET requests
-  // to prevent it from reappearing after saving the form. (Testing the request
-  // method may not be the "correct" way to accomplish this.)
-  if ($num_autochecked_items && $_SERVER['REQUEST_METHOD'] == 'GET') {
-    $args = array(
-      '%checklist' => $checklist->title,
-      '@num' => $num_autochecked_items,
-    );
-    $message = format_plural(
-      $num_autochecked_items,
-      t('%checklist found 1 unchecked item that was already completed and checked it for you. Save the form to record the change.', $args),
-      t('%checklist found @num unchecked items that were already completed and checked them for you. Save the form to record the changes.', $args)
-    );
-    drupal_set_message($message, 'status');
-  }
-
-  return $form;
-}
-
-/**
- * Form submission handler for checklistapi_checklist_form().
- */
-function checklistapi_checklist_form_submit($form, &$form_state) {
-  $form['#checklist']->saveProgress($form_state['values']['checklistapi']);
-}
-
-/**
- * Determines whether the current user is in compact mode.
- *
- * Compact mode shows checklist forms with less description text.
- *
- * Whether the user is in compact mode is determined by a cookie, which is set
- * for the user by checklistapi_compact_page().
- *
- * If the user does not have the cookie, the default value is given by the
- * system variable 'checklistapi_compact_mode', which itself defaults to FALSE.
- * This does not have a user interface to set it: it is a hidden variable which
- * can be set in the settings.php file.
- *
- * @return bool
- *   TRUE when in compact mode, or FALSE when in expanded mode.
- */
-function checklistapi_compact_mode() {
-  return isset($_COOKIE['Drupal_visitor_checklistapi_compact_mode']) ? $_COOKIE['Drupal_visitor_checklistapi_compact_mode'] : variable_get('checklistapi_compact_mode', FALSE);
-}
-
-/**
- * Menu callback: Sets whether the admin menu is in compact mode or not.
- *
- * @param string $mode
- *   (optional) The mode to set compact mode to. Accepted values are "on" and
- *   "off". Defaults to "off".
- */
-function checklistapi_compact_page($mode = 'off') {
-  user_cookie_save(array('checklistapi_compact_mode' => ($mode == 'on')));
-  drupal_goto();
-}
-
-/**
- * Returns HTML for a link to show or hide inline item descriptions.
- *
- * @ingroup themeable
- */
-function theme_checklistapi_compact_link() {
-  $output = '<div class="compact-link">';
-  if (checklistapi_compact_mode()) {
-    $output .= l(
-      t('Show item descriptions'),
-      current_path() . '/compact/off',
-      array(
-        'attributes' => array(
-          'title' => t('Expand layout to include item descriptions.'),
-        ),
-        'query' => drupal_get_destination(),
-      )
-    );
-  }
-  else {
-    $output .= l(
-      t('Hide item descriptions'),
-      current_path() . '/compact/on',
-      array(
-        'attributes' => array(
-          'title' => t('Compress layout by hiding item descriptions.'),
-        ),
-        'query' => drupal_get_destination(),
-      )
-    );
-  }
-  $output .= '</div>';
-  return $output;
-}
diff --git a/checklistapi.routing.yml b/checklistapi.routing.yml
new file mode 100644
index 0000000..ca75518
--- /dev/null
+++ b/checklistapi.routing.yml
@@ -0,0 +1,10 @@
+checklistapi.report:
+  path: /admin/reports/checklistapi
+  defaults:
+    _title: Checklists
+    _content: \Drupal\checklistapi\Controller\ChecklistapiController::report
+  requirements:
+    _permission: view checklistapi checklists report
+
+route_callbacks:
+  - \Drupal\checklistapi\Routing\ChecklistapiRouteSubscriber::routes
diff --git a/checklistapi.services.yml b/checklistapi.services.yml
new file mode 100644
index 0000000..48bb0ae
--- /dev/null
+++ b/checklistapi.services.yml
@@ -0,0 +1,5 @@
+services:
+  checklistapi.access_check:
+    class: Drupal\checklistapi\Access\ChecklistapiAccessCheck
+    tags:
+      - { name: access_check, applies_to: _checklistapi_access }
diff --git a/checklistapi_example/checklistapi_example.info b/checklistapi_example/checklistapi_example.info
deleted file mode 100644
index 9d085dc..0000000
--- a/checklistapi_example/checklistapi_example.info
+++ /dev/null
@@ -1,6 +0,0 @@
-name = Checklist API example
-description = Provides an example implementation of the Checklist API.
-core = 7.x
-package = Example modules
-dependencies[] = checklistapi
-configure = admin/config/development/checklistapi-example
diff --git a/checklistapi_example/checklistapi_example.install b/checklistapi_example/checklistapi_example.install
deleted file mode 100644
index cf4d6bb..0000000
--- a/checklistapi_example/checklistapi_example.install
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Install, update, and uninstall functions for the Checklist API Example
- * module.
- */
-
-/**
- * Implements hook_uninstall().
- */
-function checklistapi_example_uninstall() {
-  // Remove saved progress.
-  variable_del('checklistapi_checklist_example_checklist');
-}
diff --git a/checklistapi_example/checklistapi_example.module b/checklistapi_example/checklistapi_example.module
deleted file mode 100644
index 19022ad..0000000
--- a/checklistapi_example/checklistapi_example.module
+++ /dev/null
@@ -1,289 +0,0 @@
-<?php
-
-/**
- * @file
- * An example implementation of the Checklist API.
- */
-
-/**
- * Implements hook_checklistapi_checklist_info().
- *
- * Defines a checklist based on
- * @link http://buytaert.net/drupal-learning-curve Dries Buytaert's Drupal learning curve @endlink
- * .
- */
-function checklistapi_example_checklistapi_checklist_info() {
-  $definitions = array();
-  $definitions['example_checklist'] = array(
-    '#title' => t('Checklist API example'),
-    '#path' => 'admin/config/development/checklistapi-example',
-    '#description' => t('An example implementation of the Checklist API.'),
-    '#help' => t('<p>This checklist based on <a href="http://buytaert.net/drupal-learning-curve">Dries Buytaert\'s Drupal learning curve</a> is an example implementation of the <a href="http://drupal.org/project/checklistapi">Checklist API</a>.</p>'),
-    'i_suck' => array(
-      '#title' => t('I suck'),
-      '#description' => t('<p>Gain these skills to pass the <em><a href="http://headrush.typepad.com/creating_passionate_users/2005/10/getting_users_p.html">suck threshold</a></em> and start being creative with Drupal.</p>'),
-      'install_configure' => array(
-        '#title' => t('Installation and configuration of Drupal core'),
-        '#description' => t('Prepare for installation, run the installation script, and take the steps that should be done after the installation script has completed.'),
-        'handbook_page' => array(
-          '#text' => t('Installation Guide'),
-          '#path' => 'http://drupal.org/documentation/install',
-        ),
-      ),
-      'node_system' => array(
-        '#title' => t('Node system'),
-        '#description' => t('Perform a variety of operations on one or more nodes.'),
-        'handbook_page' => array(
-          '#text' => t('Manage nodes'),
-          '#path' => 'http://drupal.org/node/306808',
-        ),
-      ),
-      'block_system' => array(
-        '#title' => t('Block system'),
-        '#description' => t('Create blocks and adjust their appearance, shape, size and position.'),
-        'handbook_page' => array(
-          '#text' => t('Working with blocks (content in regions)'),
-          '#path' => 'http://drupal.org/documentation/modules/block',
-        ),
-      ),
-      'users' => array(
-        '#title' => t('Users, roles and permissions'),
-        '#description' => t('Create and manage users and access control.'),
-        'handbook_page' => array(
-          '#text' => t('Managing users'),
-          '#path' => 'http://drupal.org/node/627158',
-        ),
-      ),
-      'contrib' => array(
-        '#title' => t('Installing contributed themes and modules'),
-        '#description' => t('Customize Drupal to your tastes by adding modules and themes.'),
-        'handbook_page' => array(
-          '#text' => t('Installing modules and themes'),
-          '#path' => 'http://drupal.org/documentation/install/modules-themes',
-        ),
-      ),
-    ),
-    'i_get_by' => array(
-      '#title' => t('I get by'),
-      '#description' => t('<p>Gain these skills to pass the <em><a href="http://headrush.typepad.com/creating_passionate_users/2005/10/getting_users_p.html">passion threshold</a></em> and start kicking butt with Drupal.</p>'),
-      'upgrade_patch_monitor' => array(
-        '#title' => t('Upgrading, patching, (security) monitoring'),
-        'handbook_page_upgrading' => array(
-          '#text' => t('Upgrading from previous versions'),
-          '#path' => 'http://drupal.org/upgrade',
-        ),
-        'handbook_page_patching' => array(
-          '#text' => t('Applying patches'),
-          '#path' => 'http://drupal.org/patch/apply',
-        ),
-        'security_advisories' => array(
-          '#text' => t('Security advisories'),
-          '#path' => 'http://drupal.org/security',
-        ),
-        'handbook_page_monitoring' => array(
-          '#text' => t('Monitoring a site'),
-          '#path' => 'http://drupal.org/node/627162',
-        ),
-      ),
-      'navigation_menus_taxonomy' => array(
-        '#title' => t('Navigation, menus, taxonomy'),
-        'handbook_page_menus' => array(
-          '#text' => t('Working with Menus'),
-          '#path' => 'http://drupal.org/documentation/modules/menu',
-        ),
-        'handbook_page_taxonomy' => array(
-          '#text' => t('Organizing content with taxonomy'),
-          '#path' => 'http://drupal.org/documentation/modules/taxonomy',
-        ),
-      ),
-      'locale_i18n' => array(
-        '#title' => t('Locale and internationalization'),
-        'handbook_page' => array(
-          '#text' => t('Multilingual Guide'),
-          '#path' => 'http://drupal.org/documentation/multilingual',
-        ),
-      ),
-      'customize_front_page' => array(
-        '#title' => t('Drastically customize front page'),
-        'handbook_page' => array(
-          '#text' => t('Totally customize the LOOK of your front page'),
-          '#path' => 'http://drupal.org/node/317461',
-        ),
-      ),
-      'theme_modification' => array(
-        '#title' => t('Theme and template modifications'),
-        'handbook_page' => array(
-          '#text' => t('Theming Guide'),
-          '#path' => 'http://drupal.org/documentation/theme',
-        ),
-      ),
-    ),
-    'i_kick_butt' => array(
-      '#title' => t('I kick butt'),
-      'contribute_docs_support' => array(
-        '#title' => t('Contributing documentation and support'),
-        'handbook_page_docs' => array(
-          '#text' => t('Contribute to documentation'),
-          '#path' => 'http://drupal.org/contribute/documentation',
-        ),
-        'handbook_page_support' => array(
-          '#text' => t('Provide online support'),
-          '#path' => 'http://drupal.org/contribute/support',
-        ),
-      ),
-      'content_types_views' => array(
-        '#title' => t('Content types and views'),
-        'handbook_page_content_types' => array(
-          '#text' => t('Working with nodes, content types and fields'),
-          '#path' => 'http://drupal.org/node/717120',
-        ),
-        'handbook_page_views' => array(
-          '#text' => t('Working with Views'),
-          '#path' => 'http://drupal.org/documentation/modules/views',
-        ),
-      ),
-      'actions_workflows' => array(
-        '#title' => t('Actions and workflows'),
-        'handbook_page' => array(
-          '#text' => t('Actions and Workflows'),
-          '#path' => 'http://drupal.org/node/924538',
-        ),
-      ),
-      'development' => array(
-        '#title' => t('Theme and module development'),
-        'handbook_page_theming' => array(
-          '#text' => t('Theming Guide'),
-          '#path' => 'http://drupal.org/documentation/theme',
-        ),
-        'handbook_page_development' => array(
-          '#text' => t('Develop for Drupal'),
-          '#path' => 'http://drupal.org/documentation/develop',
-        ),
-      ),
-      'advanced_tasks' => array(
-        '#title' => t('jQuery, Form API, security audits, performance tuning'),
-        'handbook_page_jquery' => array(
-          '#text' => t('JavaScript and jQuery'),
-          '#path' => 'http://drupal.org/node/171213',
-        ),
-        'handbook_page_form_api' => array(
-          '#text' => t('Form API'),
-          '#path' => 'http://drupal.org/node/37775',
-        ),
-        'handbook_page_security' => array(
-          '#text' => t('Securing your site'),
-          '#path' => 'http://drupal.org/security/secure-configuration',
-        ),
-        'handbook_page_performance' => array(
-          '#text' => t('Managing site performance'),
-          '#path' => 'http://drupal.org/node/627252',
-        ),
-      ),
-      'contribute_code' => array(
-        '#title' => t('Contributing code, designs and patches back to Drupal'),
-        'handbook_page' => array(
-          '#text' => t('Contribute to development'),
-          '#path' => 'http://drupal.org/contribute/development',
-        ),
-      ),
-      'professional' => array(
-        '#title' => t('Drupal consultant or working for a Drupal shop'),
-      ),
-      'chx_or_unconed' => array(
-        '#title' => t(
-          "I'm a !chx or !UnConeD.",
-          array(
-            '!chx' => l(t('chx'), 'http://drupal.org/user/9446'),
-            '!UnConeD' => l(t('UnConeD'), 'http://drupal.org/user/10'),
-          )
-        ),
-      ),
-    ),
-  );
-  return $definitions;
-}
-
-/**
- * Implements hook_checklistapi_checklist_info_alter().
- *
- * Alters the checklist from checklistapi_example_checklistapi_checklist_info()
- * according to
- * @link http://www.unleashedmind.com/files/drupal-learning-curve.png sun's modifications @endlink
- * of
- * @link http://buytaert.net/drupal-learning-curve Dries Buytaert's Drupal learning curve @endlink
- * .
- */
-function checklistapi_example_checklistapi_checklist_info_alter(&$definitions) {
-  $definitions['example_checklist']['#help'] = t('<p>This checklist based on <a href="http://www.unleashedmind.com/files/drupal-learning-curve.png">sun\'s modification</a> of <a href="http://buytaert.net/drupal-learning-curve">Dries Buytaert\'s Drupal learning curve</a> is an example implementation of the <a href="http://drupal.org/project/checklistapi">Checklist API</a>.</p>');
-  $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['#title'] = t('jQuery, Form API, theme and module development');
-  $definitions['example_checklist']['i_kick_butt']['advanced_tasks'] += $definitions['example_checklist']['i_kick_butt']['development'];
-  unset($definitions['example_checklist']['i_kick_butt']['development']);
-  $definitions['example_checklist']['i_kick_butt']['contribute_code']['#title'] = t('Contributing code, designs and patches back to Drupal contrib');
-  unset($definitions['example_checklist']['i_kick_butt']['chx_or_unconed']);
-  $definitions['example_checklist']['core_contributor'] = array(
-    '#title' => t("I'm a core contributor"),
-    'contribute_core_code' => array(
-      '#title' => t('Contribute code and patches to Drupal core'),
-      'handbook_page' => array(
-        '#text' => t('Core contribution mentoring (core office hours)'),
-        '#path' => 'http://drupal.org/core-office-hours',
-      ),
-      'issue_queue' => array(
-        '#text' => t('Core issue queue'),
-        '#path' => 'http://drupal.org/project/issues/drupal',
-      ),
-    ),
-    'unit_tests' => array(
-      '#title' => t('Write unit tests to get own patch committed.'),
-      'handbook_page' => array(
-        '#text' => t('Unit Testing with Simpletest'),
-        '#path' => 'http://drupal.org/node/811254',
-      ),
-    ),
-    'review_core_patches' => array(
-      '#title' => t("Review other people's core patches, understanding coding standards."),
-      'pending_patches' => array(
-        '#text' => t('Pending patches'),
-        '#path' => 'http://drupal.org/project/issues/search/drupal?status[]=8&status[]=13&status[]=14',
-      ),
-      'handbook_page' => array(
-        '#text' => t('Coding standards'),
-        '#path' => 'http://drupal.org/coding-standards',
-      ),
-    ),
-    'security_performance' => array(
-      '#title' => t('Security audits, performance tuning.'),
-      'handbook_page_security' => $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_security'],
-      'handbook_page_performance' => $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_performance'],
-    ),
-  );
-  unset($definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_security']);
-  unset($definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_performance']);
-  $definitions['example_checklist']['core_maintainer'] = array(
-    '#title' => t("I'm trustworthy for core maintainership"),
-    'add_sub_system' => array(
-      '#title' => t('Rewrite or add a Drupal core sub-system.'),
-    ),
-    'sub_system_maintainer' => array(
-      '#title' => t('Sub-system maintainer.'),
-    ),
-    'core_branch_maintainer' => array(
-      '#title' => t('Core branch maintainer'),
-    ),
-  );
-  $definitions['example_checklist']['know_every_bit_of_core'] = array(
-    '#title' => t('I know every bit of core'),
-    'im_chx' => array(
-      '#title' => t(
-        "I'm !chx.",
-        array('!chx' => l(t('chx'), 'http://drupal.org/user/9446'))
-      ),
-    ),
-  );
-  $definitions['example_checklist']['understand_all_core_patch_implications'] = array(
-    '#title' => t('I understand all implications of a core patch'),
-    'im_chuck_norris' => array(
-      '#title' => t("I'm Chuck Norris."),
-    ),
-  );
-}
diff --git a/checklistapiexample/checklistapiexample.info.yml b/checklistapiexample/checklistapiexample.info.yml
new file mode 100644
index 0000000..07bec15
--- /dev/null
+++ b/checklistapiexample/checklistapiexample.info.yml
@@ -0,0 +1,9 @@
+name: Checklist API Example
+type: module
+description: Provides an example implementation of the Checklist API.
+version: VERSION
+package: Example modules
+core: 8.x
+dependencies:
+  - checklistapi
+configure: checklistapi.checklists.example_checklist
diff --git a/checklistapiexample/checklistapiexample.menu_links.yml b/checklistapiexample/checklistapiexample.menu_links.yml
new file mode 100644
index 0000000..8dcb84a
--- /dev/null
+++ b/checklistapiexample/checklistapiexample.menu_links.yml
@@ -0,0 +1,5 @@
+checklistapiexample.checklist:
+  title: Checklist API example
+  description: An example implementation of the Checklist API.
+  route_name: checklistapi.checklists.example_checklist
+  parent: system.admin_config_development
diff --git a/checklistapiexample/checklistapiexample.module b/checklistapiexample/checklistapiexample.module
new file mode 100644
index 0000000..54f0f31
--- /dev/null
+++ b/checklistapiexample/checklistapiexample.module
@@ -0,0 +1,289 @@
+<?php
+
+/**
+ * @file
+ * An example implementation of the Checklist API.
+ */
+
+/**
+ * Implements hook_checklistapi_checklist_info().
+ *
+ * Defines a checklist based on
+ * @link http://buytaert.net/drupal-learning-curve Dries Buytaert's Drupal learning curve @endlink
+ * .
+ */
+function checklistapiexample_checklistapi_checklist_info() {
+  $definitions = array();
+  $definitions['example_checklist'] = array(
+    '#title' => t('Checklist API example'),
+    '#path' => 'admin/config/development/checklistapi-example',
+    '#description' => t('An example implementation of the Checklist API.'),
+    '#help' => t('<p>This checklist based on <a href="http://buytaert.net/drupal-learning-curve">Dries Buytaert\'s Drupal learning curve</a> is an example implementation of the <a href="http://drupal.org/project/checklistapi">Checklist API</a>.</p>'),
+    'i_suck' => array(
+      '#title' => t('I suck'),
+      '#description' => t('<p>Gain these skills to pass the <em><a href="http://headrush.typepad.com/creating_passionate_users/2005/10/getting_users_p.html">suck threshold</a></em> and start being creative with Drupal.</p>'),
+      'install_configure' => array(
+        '#title' => t('Installation and configuration of Drupal core'),
+        '#description' => t('Prepare for installation, run the installation script, and take the steps that should be done after the installation script has completed.'),
+        'handbook_page' => array(
+          '#text' => t('Installation Guide'),
+          '#path' => 'http://drupal.org/documentation/install',
+        ),
+      ),
+      'node_system' => array(
+        '#title' => t('Node system'),
+        '#description' => t('Perform a variety of operations on one or more nodes.'),
+        'handbook_page' => array(
+          '#text' => t('Manage nodes'),
+          '#path' => 'http://drupal.org/node/306808',
+        ),
+      ),
+      'block_system' => array(
+        '#title' => t('Block system'),
+        '#description' => t('Create blocks and adjust their appearance, shape, size and position.'),
+        'handbook_page' => array(
+          '#text' => t('Working with blocks (content in regions)'),
+          '#path' => 'http://drupal.org/documentation/modules/block',
+        ),
+      ),
+      'users' => array(
+        '#title' => t('Users, roles and permissions'),
+        '#description' => t('Create and manage users and access control.'),
+        'handbook_page' => array(
+          '#text' => t('Managing users'),
+          '#path' => 'http://drupal.org/node/627158',
+        ),
+      ),
+      'contrib' => array(
+        '#title' => t('Installing contributed themes and modules'),
+        '#description' => t('Customize Drupal to your tastes by adding modules and themes.'),
+        'handbook_page' => array(
+          '#text' => t('Installing modules and themes'),
+          '#path' => 'http://drupal.org/documentation/install/modules-themes',
+        ),
+      ),
+    ),
+    'i_get_by' => array(
+      '#title' => t('I get by'),
+      '#description' => t('<p>Gain these skills to pass the <em><a href="http://headrush.typepad.com/creating_passionate_users/2005/10/getting_users_p.html">passion threshold</a></em> and start kicking butt with Drupal.</p>'),
+      'upgrade_patch_monitor' => array(
+        '#title' => t('Upgrading, patching, (security) monitoring'),
+        'handbook_page_upgrading' => array(
+          '#text' => t('Upgrading from previous versions'),
+          '#path' => 'http://drupal.org/upgrade',
+        ),
+        'handbook_page_patching' => array(
+          '#text' => t('Applying patches'),
+          '#path' => 'http://drupal.org/patch/apply',
+        ),
+        'security_advisories' => array(
+          '#text' => t('Security advisories'),
+          '#path' => 'http://drupal.org/security',
+        ),
+        'handbook_page_monitoring' => array(
+          '#text' => t('Monitoring a site'),
+          '#path' => 'http://drupal.org/node/627162',
+        ),
+      ),
+      'navigation_menus_taxonomy' => array(
+        '#title' => t('Navigation, menus, taxonomy'),
+        'handbook_page_menus' => array(
+          '#text' => t('Working with Menus'),
+          '#path' => 'http://drupal.org/documentation/modules/menu',
+        ),
+        'handbook_page_taxonomy' => array(
+          '#text' => t('Organizing content with taxonomy'),
+          '#path' => 'http://drupal.org/documentation/modules/taxonomy',
+        ),
+      ),
+      'locale_i18n' => array(
+        '#title' => t('Locale and internationalization'),
+        'handbook_page' => array(
+          '#text' => t('Multilingual Guide'),
+          '#path' => 'http://drupal.org/documentation/multilingual',
+        ),
+      ),
+      'customize_front_page' => array(
+        '#title' => t('Drastically customize front page'),
+        'handbook_page' => array(
+          '#text' => t('Totally customize the LOOK of your front page'),
+          '#path' => 'http://drupal.org/node/317461',
+        ),
+      ),
+      'theme_modification' => array(
+        '#title' => t('Theme and template modifications'),
+        'handbook_page' => array(
+          '#text' => t('Theming Guide'),
+          '#path' => 'http://drupal.org/documentation/theme',
+        ),
+      ),
+    ),
+    'i_kick_butt' => array(
+      '#title' => t('I kick butt'),
+      'contribute_docs_support' => array(
+        '#title' => t('Contributing documentation and support'),
+        'handbook_page_docs' => array(
+          '#text' => t('Contribute to documentation'),
+          '#path' => 'http://drupal.org/contribute/documentation',
+        ),
+        'handbook_page_support' => array(
+          '#text' => t('Provide online support'),
+          '#path' => 'http://drupal.org/contribute/support',
+        ),
+      ),
+      'content_types_views' => array(
+        '#title' => t('Content types and views'),
+        'handbook_page_content_types' => array(
+          '#text' => t('Working with nodes, content types and fields'),
+          '#path' => 'http://drupal.org/node/717120',
+        ),
+        'handbook_page_views' => array(
+          '#text' => t('Working with Views'),
+          '#path' => 'http://drupal.org/documentation/modules/views',
+        ),
+      ),
+      'actions_workflows' => array(
+        '#title' => t('Actions and workflows'),
+        'handbook_page' => array(
+          '#text' => t('Actions and Workflows'),
+          '#path' => 'http://drupal.org/node/924538',
+        ),
+      ),
+      'development' => array(
+        '#title' => t('Theme and module development'),
+        'handbook_page_theming' => array(
+          '#text' => t('Theming Guide'),
+          '#path' => 'http://drupal.org/documentation/theme',
+        ),
+        'handbook_page_development' => array(
+          '#text' => t('Develop for Drupal'),
+          '#path' => 'http://drupal.org/documentation/develop',
+        ),
+      ),
+      'advanced_tasks' => array(
+        '#title' => t('jQuery, Form API, security audits, performance tuning'),
+        'handbook_page_jquery' => array(
+          '#text' => t('JavaScript and jQuery'),
+          '#path' => 'http://drupal.org/node/171213',
+        ),
+        'handbook_page_form_api' => array(
+          '#text' => t('Form API'),
+          '#path' => 'http://drupal.org/node/37775',
+        ),
+        'handbook_page_security' => array(
+          '#text' => t('Securing your site'),
+          '#path' => 'http://drupal.org/security/secure-configuration',
+        ),
+        'handbook_page_performance' => array(
+          '#text' => t('Managing site performance'),
+          '#path' => 'http://drupal.org/node/627252',
+        ),
+      ),
+      'contribute_code' => array(
+        '#title' => t('Contributing code, designs and patches back to Drupal'),
+        'handbook_page' => array(
+          '#text' => t('Contribute to development'),
+          '#path' => 'http://drupal.org/contribute/development',
+        ),
+      ),
+      'professional' => array(
+        '#title' => t('Drupal consultant or working for a Drupal shop'),
+      ),
+      'chx_or_unconed' => array(
+        '#title' => t(
+          "I'm a !chx or !UnConeD.",
+          array(
+            '!chx' => l(t('chx'), 'http://drupal.org/user/9446'),
+            '!UnConeD' => l(t('UnConeD'), 'http://drupal.org/user/10'),
+          )
+        ),
+      ),
+    ),
+  );
+  return $definitions;
+}
+
+/**
+ * Implements hook_checklistapi_checklist_info_alter().
+ *
+ * Alters the checklist from checklistapiexample_checklistapi_checklist_info()
+ * according to
+ * @link http://www.unleashedmind.com/files/drupal-learning-curve.png sun's modifications @endlink
+ * of
+ * @link http://buytaert.net/drupal-learning-curve Dries Buytaert's Drupal learning curve @endlink
+ * .
+ */
+function checklistapiexample_checklistapi_checklist_info_alter(&$definitions) {
+  $definitions['example_checklist']['#help'] = t('<p>This checklist based on <a href="http://www.unleashedmind.com/files/drupal-learning-curve.png">sun\'s modification</a> of <a href="http://buytaert.net/drupal-learning-curve">Dries Buytaert\'s Drupal learning curve</a> is an example implementation of the <a href="http://drupal.org/project/checklistapi">Checklist API</a>.</p>');
+  $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['#title'] = t('jQuery, Form API, theme and module development');
+  $definitions['example_checklist']['i_kick_butt']['advanced_tasks'] += $definitions['example_checklist']['i_kick_butt']['development'];
+  unset($definitions['example_checklist']['i_kick_butt']['development']);
+  $definitions['example_checklist']['i_kick_butt']['contribute_code']['#title'] = t('Contributing code, designs and patches back to Drupal contrib');
+  unset($definitions['example_checklist']['i_kick_butt']['chx_or_unconed']);
+  $definitions['example_checklist']['core_contributor'] = array(
+    '#title' => t("I'm a core contributor"),
+    'contribute_core_code' => array(
+      '#title' => t('Contribute code and patches to Drupal core'),
+      'handbook_page' => array(
+        '#text' => t('Core contribution mentoring (core office hours)'),
+        '#path' => 'http://drupal.org/core-office-hours',
+      ),
+      'issue_queue' => array(
+        '#text' => t('Core issue queue'),
+        '#path' => 'http://drupal.org/project/issues/drupal',
+      ),
+    ),
+    'unit_tests' => array(
+      '#title' => t('Write unit tests to get own patch committed.'),
+      'handbook_page' => array(
+        '#text' => t('Unit Testing with Simpletest'),
+        '#path' => 'http://drupal.org/node/811254',
+      ),
+    ),
+    'review_core_patches' => array(
+      '#title' => t("Review other people's core patches, understanding coding standards."),
+      'pending_patches' => array(
+        '#text' => t('Pending patches'),
+        '#path' => 'http://drupal.org/project/issues/search/drupal?status[]=8&status[]=13&status[]=14',
+      ),
+      'handbook_page' => array(
+        '#text' => t('Coding standards'),
+        '#path' => 'http://drupal.org/coding-standards',
+      ),
+    ),
+    'security_performance' => array(
+      '#title' => t('Security audits, performance tuning.'),
+      'handbook_page_security' => $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_security'],
+      'handbook_page_performance' => $definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_performance'],
+    ),
+  );
+  unset($definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_security']);
+  unset($definitions['example_checklist']['i_kick_butt']['advanced_tasks']['handbook_page_performance']);
+  $definitions['example_checklist']['core_maintainer'] = array(
+    '#title' => t("I'm trustworthy for core maintainership"),
+    'add_sub_system' => array(
+      '#title' => t('Rewrite or add a Drupal core sub-system.'),
+    ),
+    'sub_system_maintainer' => array(
+      '#title' => t('Sub-system maintainer.'),
+    ),
+    'core_branch_maintainer' => array(
+      '#title' => t('Core branch maintainer'),
+    ),
+  );
+  $definitions['example_checklist']['know_every_bit_of_core'] = array(
+    '#title' => t('I know every bit of core'),
+    'im_chx' => array(
+      '#title' => t(
+        "I'm !chx.",
+        array('!chx' => l(t('chx'), 'http://drupal.org/user/9446'))
+      ),
+    ),
+  );
+  $definitions['example_checklist']['understand_all_core_patch_implications'] = array(
+    '#title' => t('I understand all implications of a core patch'),
+    'im_chuck_norris' => array(
+      '#title' => t("I'm Chuck Norris."),
+    ),
+  );
+}
diff --git a/lib/Drupal/checklistapi/Access/ChecklistapiAccessCheck.php b/lib/Drupal/checklistapi/Access/ChecklistapiAccessCheck.php
new file mode 100644
index 0000000..27d7d71
--- /dev/null
+++ b/lib/Drupal/checklistapi/Access/ChecklistapiAccessCheck.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Access\ChecklistapiAccessCheck.
+ */
+
+namespace Drupal\checklistapi\Access;
+
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+class ChecklistapiAccessCheck implements AccessInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request, AccountInterface $account) {
+    $op = $request->attributes->get('op');
+    $op = !empty($op) ? $op : 'any';
+
+    return checklistapi_checklist_access($request->attributes->get('checklist_id'), $op) ? static::ALLOW : static::DENY;
+  }
+}
diff --git a/lib/Drupal/checklistapi/ChecklistapiChecklist.php b/lib/Drupal/checklistapi/ChecklistapiChecklist.php
index 6a06b33..f22cc5f 100644
--- a/lib/Drupal/checklistapi/ChecklistapiChecklist.php
+++ b/lib/Drupal/checklistapi/ChecklistapiChecklist.php
@@ -2,15 +2,25 @@
 
 /**
  * @file
- * Class for Checklist API checklists.
+ * Contains \Drupal\checklistapi\ChecklistapiChecklist.
  */
 
+namespace Drupal\checklistapi;
+
+use Drupal\Core\Render\Element;
+use Drupal\Core\Url;
+
 /**
  * Defines the checklist class.
  */
 class ChecklistapiChecklist {
 
   /**
+   * The configuration key for saved progress.
+   */
+  const PROGRESS_CONFIG_KEY = 'progress';
+
+  /**
    * The checklist ID.
    *
    * @var string
@@ -81,14 +91,21 @@ class ChecklistapiChecklist {
   public $savedProgress;
 
   /**
+   * The configuration object for saving progress.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  public $config;
+
+  /**
    * Constructs a ChecklistapiChecklist object.
    *
    * @param array $definition
    *   A checklist definition, as returned by checklistapi_get_checklist_info().
    */
   public function __construct(array $definition) {
-    foreach (element_children($definition) as $group_key) {
-      $this->numberOfItems += count(element_children($definition[$group_key]));
+    foreach (Element::children($definition) as $group_key) {
+      $this->numberOfItems += count(Element::children($definition[$group_key]));
       $this->items[$group_key] = $definition[$group_key];
       unset($definition[$group_key]);
     }
@@ -96,7 +113,9 @@ class ChecklistapiChecklist {
       $property_name = checklistapi_strtolowercamel(drupal_substr($property_key, 1));
       $this->$property_name = $value;
     }
-    $this->savedProgress = variable_get($this->getSavedProgressVariableName(), array());
+
+    $this->config = \Drupal::config("checklistapi.progress.{$this->id}");
+    $this->savedProgress = $this->config->get($this::PROGRESS_CONFIG_KEY);
   }
 
   /**
@@ -128,8 +147,11 @@ class ChecklistapiChecklist {
    */
   public function getLastUpdatedUser() {
     if (isset($this->savedProgress['#changed_by'])) {
-      $last_updated_user = user_load($this->savedProgress['#changed_by']);
-      return theme('username', array('account' => $last_updated_user));
+      $username = array(
+        '#theme' => 'username',
+        '#account' => user_load($this->savedProgress['#changed_by']),
+      );
+      return drupal_render($username);
     }
     else {
       return t('n/a');
@@ -161,25 +183,37 @@ class ChecklistapiChecklist {
   }
 
   /**
-   * Clears the saved progress for the checklist.
+   * Gets the route name.
    *
-   * Deletes the Drupal variable containing the checklist's saved progress.
+   * @return string
+   *   The route name.
    */
-  public function clearSavedProgress() {
-    variable_del($this->getSavedProgressVariableName());
-    drupal_set_message(t('%title saved progress has been cleared.', array(
-      '%title' => $this->title,
-    )));
+  public function getRouteName() {
+    return "checklistapi.checklists.{$this->id}";
   }
 
   /**
-   * Gets the name of the Drupal variable for the checklist's saved progress.
+   * Gets the checklist form URL.
    *
-   * @return string
-   *   The Drupal variable name.
+   * @return Url
+   *   The URL to the checklist form.
+   */
+  public function getUrl() {
+    return new Url($this->getRouteName());
+  }
+
+  /**
+   * Clears the saved progress for the checklist.
+   *
+   * Deletes the Drupal configuration object containing the checklist's saved
+   * progress.
    */
-  public function getSavedProgressVariableName() {
-    return "checklistapi_checklist_{$this->id}";
+  public function clearSavedProgress() {
+    $this->config->delete();
+
+    drupal_set_message(t('%title saved progress has been cleared.', array(
+      '%title' => $this->title,
+    )));
   }
 
   /**
@@ -189,11 +223,11 @@ class ChecklistapiChecklist {
    *   TRUE if the checklist has saved progress, or FALSE if it doesn't.
    */
   public function hasSavedProgress() {
-    return (bool) variable_get($this->getSavedProgressVariableName(), FALSE);
+    return (bool) $this->config->get($this::PROGRESS_CONFIG_KEY);
   }
 
   /**
-   * Saves checklist progress to a Drupal variable.
+   * Saves checklist progress.
    *
    * @param array $values
    *   A multidimensional array of form state checklist values.
@@ -201,12 +235,13 @@ class ChecklistapiChecklist {
    * @see checklistapi_checklist_form_submit()
    */
   public function saveProgress(array $values) {
-    global $user;
+    $user = \Drupal::currentUser();
+
     $time = time();
     $num_changed_items = 0;
     $progress = array(
       '#changed' => $time,
-      '#changed_by' => $user->uid,
+      '#changed_by' => $user->id(),
       '#completed_items' => 0,
     );
 
@@ -235,7 +270,7 @@ class ChecklistapiChecklist {
             // Item is newly checked. Set new value.
             $new_item = array(
               '#completed' => $time,
-              '#uid' => $user->uid,
+              '#uid' => $user->id(),
             );
             $num_changed_items++;
           }
@@ -254,11 +289,12 @@ class ChecklistapiChecklist {
 
     // Sort array elements alphabetically so changes to the order of items in
     // checklist definitions over time don't affect the order of elements in the
-    // saved progress variable. This simplifies use with Strongarm.
+    // saved progress details. This reduces non-substantive changes to
+    // configuration files.
     ksort($progress);
 
-    variable_set($this->getSavedProgressVariableName(), $progress);
-    drupal_set_message(format_plural(
+    $this->config->set($this::PROGRESS_CONFIG_KEY, $progress)->save();
+    drupal_set_message(\Drupal::translation()->formatPlural(
       $num_changed_items,
       '%title progress has been saved. 1 item changed.',
       '%title progress has been saved. @count items changed.',
@@ -270,8 +306,8 @@ class ChecklistapiChecklist {
    * Determines whether the current user has access to the checklist.
    *
    * @param string $operation
-   *   The operation to test access for. Possible values are "view", "edit", and
-   *   "any". Defaults to "any".
+   *   (optional) The operation to test access for. Possible values are "view",
+   *   "edit", and "any". Defaults to "any".
    *
    * @return bool
    *   Returns TRUE if the user has access, or FALSE if not.
diff --git a/lib/Drupal/checklistapi/Controller/ChecklistapiController.php b/lib/Drupal/checklistapi/Controller/ChecklistapiController.php
new file mode 100644
index 0000000..77e8877
--- /dev/null
+++ b/lib/Drupal/checklistapi/Controller/ChecklistapiController.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Controller\ChecklistapiController.
+ */
+
+namespace Drupal\checklistapi\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Controller for Checklist API.
+ */
+class ChecklistapiController extends ControllerBase {
+
+  /**
+   * Returns the Checklists report.
+   *
+   * @return array
+   *   Returns a render array.
+   */
+  public function report() {
+    // Define table header.
+    $header = array(
+      array('data' => t('Checklist')),
+      array(
+        'data' => t('Progress'),
+        'class' => array(RESPONSIVE_PRIORITY_MEDIUM),
+      ),
+      array(
+        'data' => t('Last updated'),
+        'class' => array(RESPONSIVE_PRIORITY_MEDIUM),
+      ),
+      array(
+        'data' => t('Last updated by'),
+        'class' => array(RESPONSIVE_PRIORITY_LOW),
+      ),
+      array('data' => t('Operations')),
+    );
+
+    // Build table rows.
+    $rows = array();
+    $definitions = checklistapi_get_checklist_info();
+    foreach ($definitions as $id => $definition) {
+      $checklist = checklistapi_checklist_load($id);
+      $row = array();
+      $row[] = array(
+        'data' => ($checklist->userHasAccess()) ? l($checklist->title, $checklist->path) : drupal_placeholder($checklist->title),
+        'title' => (!empty($checklist->description)) ? $checklist->description : '',
+      );
+      $row[] = t('@completed of @total (@percent%)', array(
+        '@completed' => $checklist->getNumberCompleted(),
+        '@total' => $checklist->getNumberOfItems(),
+        '@percent' => round($checklist->getPercentComplete()),
+      ));
+      $row[] = $checklist->getLastUpdatedDate();
+      $row[] = $checklist->getLastUpdatedUser();
+      if ($checklist->userHasAccess('edit') && $checklist->hasSavedProgress()) {
+        $row[] = array(
+          'data' => array(
+            '#type' => 'operations',
+            '#links' => array(
+              'clear' => array(
+                'title' => t('Clear'),
+                'href' => "{$checklist->path}/clear",
+                'query' => array('destination' => 'admin/reports/checklistapi'),
+              ),
+            ),
+          ),
+        );
+      }
+      else {
+        $row[] = '';
+      }
+      $rows[] = $row;
+    }
+
+    // Compile output.
+    $output['table'] = array(
+      '#theme' => 'table',
+      '#header' => $header,
+      '#rows' => $rows,
+      '#empty' => t('No checklists available.'),
+    );
+
+    return $output;
+  }
+
+  /**
+   * Sets whether the admin menu is in compact mode or not.
+   *
+   * @param string $mode
+   *   The mode to set compact mode to. Accepted values are "on" and "off".
+   *
+   * @throws NotFoundHttpException
+   *   Throws an exception if an invalid mode is supplied.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   Return a redirect response object.
+   */
+  public function setCompactMode($mode) {
+    $all_modes = array('on', 'off');
+    if (!in_array($mode, $all_modes)) {
+      throw new NotFoundHttpException();
+    }
+
+    // Persist the setting for the current user.
+    user_cookie_save(array('checklistapi_compact_mode' => ($mode == 'on')));
+
+    // Redirect to the checklist.
+    // @todo There must be a better way than this.
+    $path = explode('/', current_path());
+    array_pop($path);
+    array_pop($path);
+    $checklist_path = implode('/', $path);
+    return $this->redirect(new Route($checklist_path));
+  }
+}
diff --git a/lib/Drupal/checklistapi/Form/ChecklistapiChecklistClearForm.php b/lib/Drupal/checklistapi/Form/ChecklistapiChecklistClearForm.php
new file mode 100644
index 0000000..5979c97
--- /dev/null
+++ b/lib/Drupal/checklistapi/Form/ChecklistapiChecklistClearForm.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Form\ChecklistapiChecklistForm.
+ */
+
+namespace Drupal\checklistapi\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+
+/**
+ * Provides a form to clear saved progress for a given checklist.
+ */
+class ChecklistapiChecklistClearForm extends ConfirmFormBase {
+
+  /**
+   * The checklist object.
+   *
+   * @var Drupal\checklistapi\ChecklistapiChecklist
+   */
+  public $checklist;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'checklistapi_checklist_clear_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to clear saved progress?');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelRoute() {
+    return $this->checklist->getUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return t('All progress details for %checklist will be erased. This action cannot be undone.', array(
+      '%checklist' => $this->checklist->title,
+    ));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Clear');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelText() {
+    return t('Cancel');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, $checklist_id = NULL) {
+    $this->checklist = checklistapi_checklist_load($checklist_id);
+    $form['#checklist'] = $this->checklist;
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    // Clear saved progress.
+    $form['#checklist']->clearSavedProgress();
+
+    // Redirect back to checklist.
+    $form_state['redirect'] = $form['#checklist']->path;
+  }
+}
diff --git a/lib/Drupal/checklistapi/Form/ChecklistapiChecklistForm.php b/lib/Drupal/checklistapi/Form/ChecklistapiChecklistForm.php
new file mode 100644
index 0000000..4d0c8d0
--- /dev/null
+++ b/lib/Drupal/checklistapi/Form/ChecklistapiChecklistForm.php
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Form\ChecklistapiChecklistForm.
+ */
+
+namespace Drupal\checklistapi\Form;
+
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Render\Element;
+
+/**
+ * Provides a checklist form.
+ */
+class ChecklistapiChecklistForm implements FormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'checklistapi_checklist_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, $checklist_id = NULL) {
+    $form['#checklist'] = $checklist = checklistapi_checklist_load($checklist_id);
+    $user_has_edit_access = $checklist->userHasAccess('edit');
+
+    // Progress bar.
+    $progress_bar = array(
+      '#theme' => 'checklistapi_progress_bar',
+      '#message' => ($checklist->hasSavedProgress()) ? t('Last updated @date by !user', array(
+        '@date' => $checklist->getLastUpdatedDate(),
+        '!user' => $checklist->getLastUpdatedUser(),
+      )) : '&nbsp;',
+      '#number_complete' => $checklist->getNumberCompleted(),
+      '#number_of_items' => $checklist->getNumberOfItems(),
+      '#percent_complete' => (int) round($checklist->getPercentComplete()),
+    );
+    $form['progress_bar'] = array(
+      '#type' => 'markup',
+      '#markup' => drupal_render($progress_bar),
+    );
+
+    // Compact mode.
+    if (checklistapi_compact_mode()) {
+      $form['#attributes']['class'] = array('compact-mode');
+    }
+    $compact_link = array('#theme' => 'checklistapi_compact_link');
+    $form['compact_mode_link'] = array(
+      '#markup' => drupal_render($compact_link),
+    );
+
+    // General properties.
+    $form['checklistapi'] = array(
+      '#attached' => array(
+        'css' => array(drupal_get_path('module', 'checklistapi') . '/checklistapi.css'),
+        'js'  => array(drupal_get_path('module', 'checklistapi') . '/checklistapi.js'),
+      ),
+      '#tree' => TRUE,
+      '#type' => 'vertical_tabs',
+    );
+
+    // Loop through groups.
+    $num_autochecked_items = 0;
+    $groups = $checklist->items;
+    foreach (Element::children($groups) as $group_key) {
+      $group = &$groups[$group_key];
+      $form[$group_key] = array(
+        '#title' => filter_xss($group['#title']),
+        '#type' => 'details',
+        '#group' => 'checklistapi',
+      );
+      if (!empty($group['#description'])) {
+        $form[$group_key]['#description'] = filter_xss_admin($group['#description']);
+      }
+
+      // Loop through items.
+      foreach (Element::children($group) as $item_key) {
+        $item = &$group[$item_key];
+        $saved_item = !empty($checklist->savedProgress[$item_key]) ? $checklist->savedProgress[$item_key] : 0;
+        // Build title.
+        $title = filter_xss($item['#title']);
+        if ($saved_item) {
+          // Append completion details.
+          $user = array(
+            '#theme' => 'username',
+            '#account' => user_load($saved_item['#uid']),
+          );
+          $title .= t(
+            '<span class="completion-details"> - Completed @time by !user</a>',
+            array(
+              '@time' => format_date($saved_item['#completed'], 'short'),
+              '!user' => drupal_render($user),
+            )
+          );
+        }
+        // Set default value.
+        $default_value = FALSE;
+        if ($saved_item) {
+          $default_value = TRUE;
+        }
+        elseif (!empty($item['#default_value'])) {
+          if ($default_value = $item['#default_value']) {
+            $num_autochecked_items++;
+          }
+        }
+        // Get description.
+        $description = (isset($item['#description'])) ? '<p>' . filter_xss_admin($item['#description']) . '</p>' : '';
+        // Append links.
+        $links = array();
+        foreach (Element::children($item) as $link_key) {
+          $link = &$item[$link_key];
+          $options = (!empty($link['#options']) && is_array($link['#options'])) ? $link['#options'] : array();
+          $links[] = l($link['#text'], $link['#path'], $options);
+        }
+        if (count($links)) {
+          $description .= '<div class="links">' . implode(' | ', $links) . '</div>';
+        }
+        // Compile the list item.
+        $form[$group_key][$item_key] = array(
+          '#attributes' => array('class' => array('checklistapi-item')),
+          '#default_value' => $default_value,
+          '#description' => filter_xss_admin($description),
+          '#disabled' => !($user_has_edit_access),
+          '#title' => filter_xss_admin($title),
+          '#type' => 'checkbox',
+          '#group' => $group_key,
+          '#parents' => array('checklistapi', $group_key, $item_key),
+        );
+      }
+    }
+
+    // Actions.
+    $form['actions'] = array(
+      '#access' => $user_has_edit_access,
+      '#type' => 'actions',
+      '#weight' => 100,
+      'save' => array(
+        '#button_type' => 'primary',
+        '#type' => 'submit',
+        '#value' => t('Save'),
+      ),
+      'clear' => array(
+        '#access' => $checklist->hasSavedProgress(),
+        '#button_type' => 'danger',
+        '#attributes' => array('class' => array('clear-saved-progress')),
+        '#submit' => array(array($this, 'clear')),
+        '#type' => 'submit',
+        '#value' => t('Clear saved progress'),
+      ),
+    );
+
+    // Alert the user of autochecked items. Only set the message on GET requests
+    // to prevent it from reappearing after saving the form. (Testing the
+    // request method may not be the "correct" way to accomplish this.)
+    if ($num_autochecked_items && $_SERVER['REQUEST_METHOD'] == 'GET') {
+      $args = array(
+        '%checklist' => $checklist->title,
+        '@num' => $num_autochecked_items,
+      );
+      $message = \Drupal::translation()->formatPlural(
+        $num_autochecked_items,
+        t('%checklist found 1 unchecked item that was already completed and checked it for you. Save the form to record the change.', $args),
+        t('%checklist found @num unchecked items that were already completed and checked them for you. Save the form to record the changes.', $args)
+      );
+      drupal_set_message($message, 'status');
+    }
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $form['#checklist']->saveProgress($form_state['values']['checklistapi']);
+  }
+
+  /**
+   * Form submission handler for the 'clear' action.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  public function clear(array $form, array &$form_state) {
+    $form_state['redirect_route']['route_name'] = $form['#checklist']->getRouteName() . '.clear';
+  }
+
+}
diff --git a/lib/Drupal/checklistapi/Routing/ChecklistapiRouteSubscriber.php b/lib/Drupal/checklistapi/Routing/ChecklistapiRouteSubscriber.php
new file mode 100644
index 0000000..b778a2c
--- /dev/null
+++ b/lib/Drupal/checklistapi/Routing/ChecklistapiRouteSubscriber.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Routing\ChecklistapiRouteSubscriber.
+ */
+
+namespace Drupal\checklistapi\Routing;
+
+use Drupal\Core\Routing\RouteSubscriberBase;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Listens to the dynamic route events.
+ */
+class ChecklistapiRouteSubscriber extends RouteSubscriberBase {
+
+  /**
+   * Provides dynamic routes for Checklist API.
+   *
+   * @return \Symfony\Component\Routing\Route[]
+   *   An array of route objects.
+   */
+  public function routes() {
+    $routes = array();
+    foreach (checklistapi_get_checklist_info() as $id => $definition) {
+      // Ignore incomplete definitions.
+      if (empty($definition['#path']) || empty($definition['#title'])) {
+        continue;
+      }
+
+      // View/edit checklist.
+      $routes["checklistapi.checklists.{$id}"] = new Route($definition['#path'], array(
+        '_title' => $definition['#title'],
+        '_form' => '\Drupal\checklistapi\Form\ChecklistapiChecklistForm',
+        'checklist_id' => $id,
+        'op' => 'any',
+      ), $requirements = array('_checklistapi_access' => 'TRUE'));
+
+      // Clear saved progress.
+      $routes["checklistapi.checklists.{$id}.clear"] = new Route("{$definition['#path']}/clear", array(
+        '_title' => 'Clear',
+        '_form' => '\Drupal\checklistapi\Form\ChecklistapiChecklistClearForm',
+        'checklist_id' => $id,
+        'op' => 'edit',
+      ), $requirements);
+
+      // Toggle compact mode.
+      $routes["checklistapi.checklists.{$id}.compact"] = new Route("{$definition['#path']}/compact/{mode}", array(
+        '_title' => 'Compact mode',
+        '_content' => '\Drupal\checklistapi\Controller\ChecklistapiController::setCompactMode',
+        'checklist_id' => $id,
+        'op' => 'any',
+        'mode' => 'off',
+      ), $requirements);
+
+      return $routes;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function alterRoutes(RouteCollection $collection, $provider) {}
+}
diff --git a/lib/Drupal/checklistapi/Tests/ChecklistapiTest.php b/lib/Drupal/checklistapi/Tests/ChecklistapiTest.php
new file mode 100644
index 0000000..310f690
--- /dev/null
+++ b/lib/Drupal/checklistapi/Tests/ChecklistapiTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Tests\ChecklistapiTest.
+ */
+
+namespace Drupal\checklistapi\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Functionally tests Checklist API.
+ *
+ * @todo Add tests for vertical tabs progress indicators.
+ * @todo Add tests for saving and retrieving checklist progress.
+ * @todo Add tests for clearing saved progress.
+ */
+class ChecklistapiTest extends WebTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array('checklistapi', 'checklistapiexample', 'help', 'block');
+
+  /**
+   * @var \Drupal\user\Entity\User
+   *   A user object with permission to edit any checklist.
+   */
+  protected $privilegedUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Checklist API functionality',
+      'description' => 'Tests Checklist API functionality.',
+      'group' => 'Checklist API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create a privileged user.
+    $permissions = array('edit any checklistapi checklist');
+    $this->privilegedUser = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->privilegedUser);
+
+    // Place help block.
+    $this->drupalPlaceBlock('system_help_block', array(
+      'label' => '',
+      'region' => 'help',
+    ));
+  }
+
+  /**
+   * Tests checklist access.
+   */
+  public function testChecklistAccess() {
+    $this->drupalGet('admin/config/development/checklistapi-example');
+    $this->assertResponse(200, 'Granted access to user with "edit any checklistapi checklist" permission.');
+
+    $permissions = array('edit example_checklist checklistapi checklist');
+    $semi_privileged_user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($semi_privileged_user);
+    $this->drupalGet('admin/config/development/checklistapi-example');
+    $this->assertResponse(200, 'Granted access to user with checklist-specific permission.');
+
+    $this->drupalLogout();
+    $this->drupalGet('admin/config/development/checklistapi-example');
+    $this->assertResponse(403, 'Denied access to non-privileged user.');
+  }
+
+  /**
+   * Tests checklist composition.
+   */
+  public function testChecklistComposition() {
+    $permissions = array('edit example_checklist checklistapi checklist');
+    $this->assertTrue($this->checkPermissions($permissions), 'Created per-checklist permission.');
+
+    $this->drupalGet('admin/config/development/checklistapi-example');
+    $this->assertRaw('This checklist based on', 'Created per-checklist help block.');
+  }
+
+}
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 0000000..5a4c9ca
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env sh
+
+# @file
+# Runs unit tests for Checklist API module.
+#
+# This script assumes that the module is installed in
+# /path-to-drupal-docroot/modules/contrib/checklistapi.
+
+cd ../../../core/
+./vendor/bin/phpunit \
+  --coverage-html ../modules/contrib/checklistapi/coverage \
+  ../modules/contrib/checklistapi/tests/Drupal/checklistapi/Tests/
diff --git a/templates/checklistapi-compact-link.html.twig b/templates/checklistapi-compact-link.html.twig
new file mode 100644
index 0000000..447ed31
--- /dev/null
+++ b/templates/checklistapi-compact-link.html.twig
@@ -0,0 +1,18 @@
+{#
+/**
+ * @file
+ * Default theme implementation of a checklist compact link.
+ *
+ * Available variables:
+ * - link: The link, already formatted by l().
+ *
+ * @see template_preprocess_checklistapi_compact_link()
+ *
+ * @ingroup themeable
+ */
+#}
+{% spaceless %}
+  <div class="compact-link">
+    {{ link }}
+  </div>
+{% endspaceless %}
diff --git a/templates/checklistapi-progress-bar.html.twig b/templates/checklistapi-progress-bar.html.twig
new file mode 100644
index 0000000..919a9ad
--- /dev/null
+++ b/templates/checklistapi-progress-bar.html.twig
@@ -0,0 +1,19 @@
+{#
+/**
+ * @file
+ * Default theme implementation of a checklist progress bar.
+ *
+ * Available variables:
+ * - message: A string containing information to be displayed.
+ * - number_complete: The number of items completed.
+ * - number_of_items: The total number of items in the checklist.
+ * - percent_complete: The percentage of the progress.
+ *
+ * @ingroup themeable
+ */
+#}
+<div class="progress" aria-live="polite">
+  <div class="progress__track"><div class="progress__bar" style="width: {{ percent_complete }}%;"></div></div>
+  <div class="progress__percentage">{{ number_complete }} of {{ number_of_items }} ({{ percent_complete }}%)</div>
+  <div class="progress__description">{{ message }}</div>
+</div>
diff --git a/templates/checklistapi-progress-bar.tpl.php b/templates/checklistapi-progress-bar.tpl.php
deleted file mode 100644
index 383dbf8..0000000
--- a/templates/checklistapi-progress-bar.tpl.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Default theme implementation for the Checklist API progress bar.
- *
- * Available variables:
- * - $message: The progress message.
- * - $number_complete: The number of items complete.
- * - $number_of_items: The total number of items.
- * - $percent_complete: The percent of items complete.
- *
- * @see template_preprocess()
- * @see template_preprocess_checklistapi_progress_bar()
- * @see template_process()
- */
-?>
-<div class="progress">
-  <div class="bar"><div class="filled" style="width:<?php print $percent_complete; ?>%;"></div></div>
-  <div class="percentage"><?php print $number_complete; ?> of <?php print $number_of_items; ?> (<?php print $percent_complete; ?>%)</div>
-  <div class="message"><?php print $message; ?></div>
-</div>
diff --git a/tests/Drupal/checklistapi/Tests/ChecklistapiControllerTest.php b/tests/Drupal/checklistapi/Tests/ChecklistapiControllerTest.php
new file mode 100644
index 0000000..f0ed417
--- /dev/null
+++ b/tests/Drupal/checklistapi/Tests/ChecklistapiControllerTest.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Tests\ChecklistapiControllerTest.
+ */
+
+namespace Drupal\checklistapi\Tests;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\checklistapi\Controller\ChecklistapiController;
+
+/**
+ * Tests the ChecklistapiController class.
+ */
+class ChecklistapiControllerTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'ChecklistapiController class',
+      'description' => 'Test the ChecklistapiController class.',
+      'group' => 'Checklist API',
+    );
+  }
+
+  /**
+   * Tests that setCompactMode() rejects an invalid mode.
+   *
+   * @covers \Drupal\checklistapi\Controller\ChecklistapiController::setCompactMode()
+   * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   */
+  public function testSetCompactModeToInvalidMode() {
+    $controller = new ChecklistapiController();
+    $controller->setCompactMode('invalid mode');
+  }
+
+}
diff --git a/tests/Drupal/checklistapi/Tests/ChecklistapiModuleTest.php b/tests/Drupal/checklistapi/Tests/ChecklistapiModuleTest.php
new file mode 100644
index 0000000..4998eec
--- /dev/null
+++ b/tests/Drupal/checklistapi/Tests/ChecklistapiModuleTest.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Tests\ChecklistapiModuleTest.
+ */
+
+namespace Drupal\checklistapi\Tests;
+
+use Drupal\Core\Render\Element;
+use Drupal\Tests\UnitTestCase;
+
+require_once __DIR__ . '/../../../../checklistapi.module';
+
+/**
+ * Tests the functions in checklistapi.module.
+ */
+class ChecklistapiModuleTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Checklist API module',
+      'description' => 'Test checklistapi.module.',
+      'group' => 'Checklist API',
+    );
+  }
+
+  /**
+   * Tests checklistapi_sort_array().
+   */
+  public function testChecklistapiSortArray() {
+    $input = array(
+      '#title' => 'Checklist API test',
+      '#path' => 'admin/config/development/checklistapi-test',
+      '#description' => 'A test checklist.',
+      '#help' => '<p>This is a test checklist.</p>',
+      'group_two' => array(
+        '#title' => 'Group two',
+      ),
+      'group_one' => array(
+        '#title' => 'Group one',
+        '#description' => '<p>Group one description.</p>',
+        '#weight' => -1,
+        'item_three' => array(
+          '#title' => 'Item three',
+          '#weight' => 1,
+        ),
+        'item_one' => array(
+          '#title' => 'Item one',
+          '#description' => 'Item one description',
+          '#weight' => -1,
+          'link_three' => array(
+            '#text' => 'Link three',
+            '#path' => 'http://example.com/three',
+            '#weight' => 3,
+          ),
+          'link_two' => array(
+            '#text' => 'Link two',
+            '#path' => 'http://example.com/two',
+            '#weight' => 2,
+          ),
+          'link_one' => array(
+            '#text' => 'Link one',
+            '#path' => 'http://example.com/one',
+            '#weight' => 1,
+          ),
+        ),
+        'item_two' => array(
+          '#title' => 'Item two',
+        ),
+      ),
+      'group_four' => array(
+        '#title' => 'Group four',
+        '#weight' => 1,
+      ),
+      'group_three' => array(
+        '#title' => 'Group three',
+        '#weight' => 'invalid',
+      ),
+    );
+
+    $output = checklistapi_sort_array($input);
+
+    $this->assertEquals(0, $output['group_two']['#weight'], 'Failed to supply a default for omitted element weight.');
+    $this->assertEquals(0, $output['group_three']['#weight'], 'Failed to supply a default in place of invalid element weight.');
+    $this->assertEquals(-1, $output['group_one']['#weight'], 'Failed to retain a valid element weight.');
+    $this->assertEquals(
+      array('group_one', 'group_two', 'group_three', 'group_four'),
+      Element::children($output),
+      'Failed to sort elements by weight.'
+    );
+    $this->assertEquals(
+      array('link_one', 'link_two', 'link_three'),
+      Element::children($output['group_one']['item_one']),
+      'Failed to recurse through element descendants.'
+    );
+  }
+
+  /**
+   * Tests checklistapi_strtolowercamel().
+   */
+  public function testChecklistapiStrtolowercamel() {
+    $this->assertEquals('abcDefGhi', checklistapi_strtolowercamel('Abc def_ghi'), 'Failed to convert string to lowerCamel case.');
+  }
+
+  /**
+   * Tests that checklistapi_checklist_access() rejects an invalid mode.
+   *
+   * @expectedException \InvalidArgumentException
+   * @expectedExceptionMessage No such operation "invalid operation"
+   */
+  public function testChecklistapiChecklistAccessInvalidMode() {
+    checklistapi_checklist_access(NULL, 'invalid operation');
+  }
+
+}
diff --git a/tests/checklistapi.test b/tests/checklistapi.test
deleted file mode 100644
index d96c3f1..0000000
--- a/tests/checklistapi.test
+++ /dev/null
@@ -1,127 +0,0 @@
-<?php
-
-/**
- * @file
- * Tests for Checklist API module.
- */
-
-/**
- * Unit tests for Checklist API.
- */
-class ChecklistapiUnitTestCase extends DrupalUnitTestCase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function getInfo() {
-    return array(
-      'name' => 'Unit tests',
-      'description' => 'Test Checklist API classes and functions.',
-      'group' => 'Checklist API',
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    drupal_load('module', 'checklistapi');
-    drupal_load('module', 'checklistapi_test');
-    parent::setUp();
-  }
-
-  /**
-   * Test checklistapi_sort_array().
-   */
-  public function testChecklistapiSortArray() {
-    $checklistapi_test_definition = checklistapi_test_checklistapi_checklist_info();
-    $input = array_pop($checklistapi_test_definition);
-    $output = checklistapi_sort_array($input);
-    $this->assertEqual($output['group_two']['#weight'], 0, 'Supplied a default for omitted element weight.');
-    $this->assertEqual($output['group_three']['#weight'], 0, 'Supplied a default in place of invalid element weight.');
-    $this->assertEqual($output['group_one']['#weight'], -1, 'Retained a valid element weight.');
-    $this->assertEqual(
-      element_children($output),
-      array('group_one', 'group_two', 'group_three', 'group_four'),
-      'Sorted elements by weight.'
-    );
-    $this->assertEqual(
-      element_children($output['group_one']['item_one']),
-      array('link_one', 'link_two', 'link_three'),
-      'Recursed through element descendants.'
-    );
-  }
-
-  /**
-   * Test checklistapi_strtolowercamel().
-   */
-  public function testChecklistapiStrtolowercamel() {
-    $this->assertEqual(checklistapi_strtolowercamel('Abc def_ghi'), 'abcDefGhi', 'Converted string to lowerCamel case.');
-  }
-
-}
-
-/**
- * Functional tests for Checklist API.
- *
- * @todo Add tests for vertical tabs progress indicators.
- * @todo Add tests for saving and retrieving checklist progress.
- * @todo Add tests for clearing saved progress.
- */
-class ChecklistapiWebTestCase extends DrupalWebTestCase {
-  protected $privilegedUser;
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function getInfo() {
-    return array(
-      'name' => 'Functional tests',
-      'description' => 'Test the functionality of Checklist API.',
-      'group' => 'Checklist API',
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp('checklistapi_example');
-    $permissions = array('edit any checklistapi checklist');
-    $this->privilegedUser = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->privilegedUser);
-  }
-
-  /**
-   * Test checklist access.
-   */
-  public function testAccessChecklist() {
-    $this->drupalGet('admin/config/development/checklistapi-example');
-    $this->assertResponse(200, 'Granted access to user with "edit any checklistapi checklist" permission.');
-
-    $permissions = array('edit example_checklist checklistapi checklist');
-    $semi_privileged_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($semi_privileged_user);
-    $this->drupalGet('admin/config/development/checklistapi-example');
-    $this->assertResponse(200, 'Granted access to user with checklist-specific permission.');
-
-    $this->drupalLogout();
-    $this->drupalGet('admin/config/development/checklistapi-example');
-    $this->assertResponse(403, 'Denied access to nonprivileged user.');
-  }
-
-  /**
-   * Test checklist composition.
-   */
-  public function testChecklistComposition() {
-    $menu_item = menu_get_item('admin/config/development/checklistapi-example');
-    $this->assertEqual($menu_item['path'], 'admin/config/development/checklistapi-example', 'Created per-checklist menu item.');
-
-    $permissions = array('edit example_checklist checklistapi checklist');
-    $this->assertTrue($this->checkPermissions($permissions), 'Created per-checklist permission.');
-
-    $this->drupalGet('admin/config/development/checklistapi-example');
-    $this->assertRaw('id="block-system-help"', 'Created per-checklist help block.');
-  }
-
-}
diff --git a/tests/modules/checklistapi_test/checklistapi_test.info b/tests/modules/checklistapi_test/checklistapi_test.info
deleted file mode 100644
index b4f1394..0000000
--- a/tests/modules/checklistapi_test/checklistapi_test.info
+++ /dev/null
@@ -1,7 +0,0 @@
-name = Checklist API test module
-description = Provides an implementation of the Checklist API for testing.
-package = Testing
-version = VERSION
-core = 7.x
-dependencies[] = checklistapi
-hidden = TRUE
diff --git a/tests/modules/checklistapi_test/checklistapi_test.module b/tests/modules/checklistapi_test/checklistapi_test.module
deleted file mode 100644
index 01c9dbe..0000000
--- a/tests/modules/checklistapi_test/checklistapi_test.module
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module for Checklist API.
- */
-
-/**
- * Implements hook_checklistapi_checklist_info().
- */
-function checklistapi_test_checklistapi_checklist_info() {
-  $definitions = array();
-  $definitions['test_checklist'] = array(
-    '#title' => t('Checklist API test'),
-    '#path' => 'admin/config/development/checklistapi-test',
-    '#description' => t('A test checklist.'),
-    '#help' => t('<p>This is a test checklist.</p>'),
-    'group_two' => array(
-      '#title' => t('Group two'),
-    ),
-    'group_one' => array(
-      '#title' => t('Group one'),
-      '#description' => t('<p>Group one description.</p>'),
-      '#weight' => -1,
-      'item_three' => array(
-        '#title' => t('Item three'),
-        '#weight' => 1,
-      ),
-      'item_one' => array(
-        '#title' => t('Item one'),
-        '#description' => t('Item one description'),
-        '#weight' => -1,
-        'link_three' => array(
-          '#text' => t('Link three'),
-          '#path' => 'http://example.com/three',
-          '#weight' => 3,
-        ),
-        'link_two' => array(
-          '#text' => t('Link two'),
-          '#path' => 'http://example.com/two',
-          '#weight' => 2,
-        ),
-        'link_one' => array(
-          '#text' => t('Link one'),
-          '#path' => 'http://example.com/one',
-          '#weight' => 1,
-        ),
-      ),
-      'item_two' => array(
-        '#title' => t('Item two'),
-      ),
-    ),
-    'group_four' => array(
-      '#title' => t('Group four'),
-      '#weight' => 1,
-    ),
-    'group_three' => array(
-      '#title' => t('Group three'),
-      '#weight' => 'invalid',
-    ),
-  );
-  return $definitions;
-}
