diff --git a/README.txt b/README.txt
index 921c0d3..e687877 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
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.css b/checklistapi.css
index b7cc6ea..00c8327 100644
--- a/checklistapi.css
+++ b/checklistapi.css
@@ -1,4 +1,3 @@
-
 #checklistapi-checklist-form div.description p {
   margin: .5em 0;
 }
@@ -19,7 +18,6 @@
   font-weight: normal;
   margin-bottom: 0.5em;
 }
-#checklistapi-checklist-form .progress .bar,
-#checklistapi-checklist-form .progress .filled {
-  background-image: none;
+#checklistapi-checklist-form .compact-link {
+  clear: both;
 }
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..95cfad4 100644
--- a/checklistapi.js
+++ b/checklistapi.js
@@ -6,7 +6,7 @@
    */
   Drupal.behaviors.checklistapiFieldsetSummaries = {
     attach: function (context) {
-      $('#checklistapi-checklist-form .vertical-tabs-panes > fieldset', context).drupalSetSummary(function (context) {
+      $('#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();
diff --git a/checklistapi.module b/checklistapi.module
index c229177..65ef951 100644
--- a/checklistapi.module
+++ b/checklistapi.module
@@ -8,6 +8,96 @@
  * completion times and users.
  */
 
+use Drupal\checklistapi\ChecklistapiChecklist;
+use Drupal\Core\Render\Element;
+
+/**
+ * Implements hook_menu_link_defaults().
+ */
+function checklistapi_menu_link_defaults() {
+  $links = array();
+
+  // Checklists report.
+  $links['admin/reports/checklistapi'] = array(
+    'link_title' => 'Checklists',
+    'description' => 'Get an overview of your installed checklists with progress details.',
+    'route_name' => 'checklistapi.report',
+    'parent' => 'system.admin.reports',
+  );
+
+  return $links;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function checklistapi_permission() {
+  $perms = array();
+
+  // Universal permissions.
+  $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'))
+    ),
+  );
+  $perms['view any checklistapi checklist'] = array(
+    'title' => t('View any checklist'),
+    'description' => $view_checklist_perm_description = t('Read-only access: View list items and saved progress.'),
+  );
+  $perms['edit any checklistapi checklist'] = array(
+    'title' => t('Edit any checklist'),
+    'description' => $edit_checklist_perm_description = t('Check and uncheck list items and save changes, or clear saved progress.'),
+  );
+
+  // Per checklist permissions.
+  foreach (checklistapi_get_checklist_info() as $id => $definition) {
+    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']))
+      ),
+      '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']))
+      ),
+      'description' => $edit_checklist_perm_description,
+    );
+  }
+
+  return $perms;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function checklistapi_theme() {
+  return array(
+    'checklistapi_compact_link' => array(
+      'template' => 'checklistapi-compact-link',
+      'variables' => array(
+        'link' => '',
+      ),
+    ),
+    'checklistapi_progress_bar' => array(
+      'path' => drupal_get_path('module', 'checklistapi') . '/templates',
+      'template' => 'checklistapi-progress-bar',
+      'variables' => array(
+        'message' => '&nbsp;',
+        'number_complete' => 0,
+        'number_of_items' => 0,
+        'percent_complete' => 0,
+      ),
+    ),
+  );
+}
+
 /**
  * Access callback: Checks the current user's access to a given checklist.
  *
@@ -20,6 +110,9 @@
  * @return bool
  *   Returns TRUE if the current user has access to perform a given operation on
  *   the specified checklist, or FALSE if not.
+ *
+ * @throws Exception
+ *   Throws an exception if an unsupported operation is supplied.
  */
 function checklistapi_checklist_access($id, $operation = 'any') {
   $all_operations = array('view', 'edit', 'any');
@@ -60,10 +153,12 @@ function checklistapi_checklist_load($id) {
  *   exists, or an array of all checklist definitions if none is specified.
  */
 function checklistapi_get_checklist_info($id = NULL) {
-  $definitions = &drupal_static(__FUNCTION__);
-  if (!isset($definitions)) {
-    // Get definitions.
-    $definitions = module_invoke_all('checklistapi_checklist_info');
+  static $drupal_static_definitions;
+  if (!isset($drupal_static_definitions)) {
+    $drupal_static_definitions['definitions'] = &drupal_static(__FUNCTION__);
+  }
+  if (!isset($drupal_static_definitions['definitions'])) {
+    $definitions = \Drupal::moduleHandler()->invokeAll('checklistapi_checklist_info');
     $definitions = checklistapi_sort_array($definitions);
     // Let other modules alter them.
     drupal_alter('checklistapi_checklist_info', $definitions);
@@ -80,131 +175,24 @@ function checklistapi_get_checklist_info($id = NULL) {
 }
 
 /**
- * Implements hook_help().
- */
-function checklistapi_help($path, $arg) {
-  foreach (checklistapi_get_checklist_info() as $definition) {
-    if ($definition['#path'] == $path && !empty($definition['#help'])) {
-      return $definition['#help'];
-    }
-  }
-}
-
-/**
- * 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().
+ * 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_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() {
-  $perms = array();
-
-  // Universal permissions.
-  $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'))
-    ),
-  );
-  $perms['view any checklistapi checklist'] = array(
-    'title' => t('View any checklist'),
-    'description' => $view_checklist_perm_description = t('Read-only access: View list items and saved progress.'),
-  );
-  $perms['edit any checklistapi checklist'] = array(
-    'title' => t('Edit any checklist'),
-    'description' => $edit_checklist_perm_description = t('Check and uncheck list items and save changes, or clear saved progress.'),
-  );
-
-  // Per checklist permissions.
-  foreach (checklistapi_get_checklist_info() as $id => $definition) {
-    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']))
-      ),
-      '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']))
-      ),
-      'description' => $edit_checklist_perm_description,
-    );
-  }
-
-  return $perms;
+function checklistapi_compact_mode() {
+  $config = \Drupal::config('checklistapi.progress');
+  return isset($_COOKIE['Drupal_visitor_checklistapi_compact_mode']) ? $_COOKIE['Drupal_visitor_checklistapi_compact_mode'] : $config->get('checklistapi_compact_mode');
 }
 
 /**
@@ -220,7 +208,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.
@@ -269,29 +257,34 @@ 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;
 }
 
 /**
- * Implements hook_theme().
+ * Prepares variables for checklist compact link templates.
+ *
+ * Default template: checklistapi-compact-link.html.twig.
+ *
+ * @param array $variables
+ *   An empty array.
  */
-function checklistapi_theme() {
-  return array(
-    'checklistapi_compact_link' => array(
-      'file' => 'checklistapi.pages.inc',
-    ),
-    'checklistapi_progress_bar' => array(
-      'path' => drupal_get_path('module', 'checklistapi') . '/templates',
-      'template' => 'checklistapi-progress-bar',
-      'variables' => array(
-        'message' => '&nbsp;',
-        'number_complete' => 0,
-        'number_of_items' => 0,
-        'percent_complete' => 0,
-      ),
+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..46df682
--- /dev/null
+++ b/checklistapi.routing.yml
@@ -0,0 +1,10 @@
+checklistapi.report:
+  path: /admin/reports/checklistapi
+  defaults:
+    _content: \Drupal\checklistapi\Controller\ChecklistapiController::report
+    _title: Checklists
+  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.info.yml b/checklistapi_example/checklistapi_example.info.yml
new file mode 100644
index 0000000..4bd0aac
--- /dev/null
+++ b/checklistapi_example/checklistapi_example.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/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/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..340dcf1 100644
--- a/lib/Drupal/checklistapi/ChecklistapiChecklist.php
+++ b/lib/Drupal/checklistapi/ChecklistapiChecklist.php
@@ -5,6 +5,11 @@
  * Class for Checklist API checklists.
  */
 
+namespace Drupal\checklistapi;
+
+use Drupal\Core\Render\Element;
+use Drupal\Core\Url;
+
 /**
  * Defines the checklist class.
  */
@@ -81,14 +86,23 @@ 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]));
+    $this->config = \Drupal::config('checklistapi.progress');
+
+    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 +110,7 @@ class ChecklistapiChecklist {
       $property_name = checklistapi_strtolowercamel(drupal_substr($property_key, 1));
       $this->$property_name = $value;
     }
-    $this->savedProgress = variable_get($this->getSavedProgressVariableName(), array());
+    $this->savedProgress = $this->config->get($this->getSavedProgressVariableName());
   }
 
   /**
@@ -128,8 +142,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,12 +178,33 @@ class ChecklistapiChecklist {
   }
 
   /**
+   * Gets the route name.
+   *
+   * @return string
+   *   The route name.
+   */
+  public function getRouteName() {
+    return "checklistapi.checklists.{$this->id}";
+  }
+
+  /**
+   * Gets the checklist form URL.
+   *
+   * @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 variable containing the checklist's saved progress.
    */
   public function clearSavedProgress() {
-    variable_del($this->getSavedProgressVariableName());
+    $this->config->set($this->getSavedProgressVariableName(), 0)->save();
+
     drupal_set_message(t('%title saved progress has been cleared.', array(
       '%title' => $this->title,
     )));
@@ -189,7 +227,7 @@ 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->getSavedProgressVariableName());
   }
 
   /**
@@ -201,12 +239,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 +274,7 @@ class ChecklistapiChecklist {
             // Item is newly checked. Set new value.
             $new_item = array(
               '#completed' => $time,
-              '#uid' => $user->uid,
+              '#uid' => $user->id(),
             );
             $num_changed_items++;
           }
@@ -257,13 +296,15 @@ class ChecklistapiChecklist {
     // saved progress variable. This simplifies use with Strongarm.
     ksort($progress);
 
-    variable_set($this->getSavedProgressVariableName(), $progress);
+    $this->config->set($this->getSavedProgressVariableName(), $progress);
     drupal_set_message(format_plural(
       $num_changed_items,
       '%title progress has been saved. 1 item changed.',
       '%title progress has been saved. @count items changed.',
       array('%title' => $this->title)
     ));
+
+    $this->config->save();
   }
 
   /**
diff --git a/lib/Drupal/checklistapi/Controller/ChecklistapiController.php b/lib/Drupal/checklistapi/Controller/ChecklistapiController.php
new file mode 100644
index 0000000..4ad2119
--- /dev/null
+++ b/lib/Drupal/checklistapi/Controller/ChecklistapiController.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Controller\ChecklistapiController.
+ */
+
+namespace Drupal\checklistapi\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Controller for Checklist API.
+ */
+class ChecklistapiController extends ControllerBase {
+
+  /**
+   * Returns the Checklists report.
+   */
+  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;
+  }
+
+  /**
+   * Callback for the checklistapi.definition_{checklist_id}_compact route.
+   */
+  public function setCompact($mode) {
+    user_cookie_save(array('checklistapi_compact_mode' => ($mode == 'on')));
+    return $this->redirect('<front>');
+  }
+}
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..bad5a05
--- /dev/null
+++ b/lib/Drupal/checklistapi/Form/ChecklistapiChecklistForm.php
@@ -0,0 +1,198 @@
+<?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);
+
+    // 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' => 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 = $checklist->userHasAccess('edit')),
+          '#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 = 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;
+  }
+
+  /**
+   * {@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..85029f7
--- /dev/null
+++ b/lib/Drupal/checklistapi/Routing/ChecklistapiRouteSubscriber.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\checklistapi\Routing\ChecklistapiRouteSubscriber.
+ */
+
+namespace Drupal\checklistapi\Routing;
+
+use Drupal\Core\Routing\RouteSubscriberBase;
+use Symfony\Component\Routing\Route;
+
+/**
+ * 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::setCompact',
+        'checklist_id' => $id,
+        'op' => 'any',
+        'mode' => 'off',
+      ), $requirements);
+
+      return $routes;
+    }
+  }
+}
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/checklistapi.test b/tests/checklistapi.test
index d96c3f1..631753f 100644
--- a/tests/checklistapi.test
+++ b/tests/checklistapi.test
@@ -5,6 +5,8 @@
  * Tests for Checklist API module.
  */
 
+use Drupal\Core\Render\Element;
+
 /**
  * Unit tests for Checklist API.
  */
@@ -41,12 +43,12 @@ class ChecklistapiUnitTestCase extends DrupalUnitTestCase {
     $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),
+      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']),
+      Element::children($output['group_one']['item_one']),
       array('link_one', 'link_two', 'link_three'),
       'Recursed through element descendants.'
     );
