diff --git a/diff.views.inc b/diff.views.inc
new file mode 100644
index 0000000..25ecec4
--- /dev/null
+++ b/diff.views.inc
@@ -0,0 +1,36 @@
+<?php
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Implements hook_views_data().
+ */
+function diff_views_data() {
+  $data = [];
+  $entity_types = \Drupal::entityTypeManager()->getDefinitions();
+  $revisionable_entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
+    return $entity_type->isRevisionable();
+  });
+
+  /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
+  foreach ($revisionable_entity_types as $entity_type) {
+    $revision_base_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
+
+    $data[$revision_base_table]['diff_from'] = [
+      'title' => t('Diff from'),
+      'help' => '',
+      'field' => [
+        'id' => 'diff__from',
+      ],
+    ];
+
+    $data[$revision_base_table]['diff_to'] = [
+      'title' => t('Diff to'),
+      'help' => '',
+      'field' => [
+        'id' => 'diff__to',
+      ],
+    ];
+  }
+
+  return $data;
+}
diff --git a/tests/modules/diff_test/config/install/views.view.test_diff_view.yml b/tests/modules/diff_test/config/install/views.view.test_diff_view.yml
new file mode 100644
index 0000000..225fd6e
--- /dev/null
+++ b/tests/modules/diff_test/config/install/views.view.test_diff_view.yml
@@ -0,0 +1,418 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - diff
+    - node
+    - user
+id: test_diff_view
+label: 'test diff view'
+module: views
+description: ''
+tag: ''
+base_table: node_field_revision
+base_field: vid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'view all revisions'
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: full
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: '‹ Previous'
+            next: 'Next ›'
+            first: '« First'
+            last: 'Last »'
+          quantity: 9
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            changed: changed
+            title: title
+            diff_from: diff_from
+            diff_to: diff_to
+          info:
+            changed:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            title:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            diff_from:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            diff_to:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: '-1'
+          empty_table: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        diff_from:
+          id: diff_from
+          table: node_field_revision
+          field: diff_from
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: false
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          entity_type: node
+          plugin_id: diff__from
+        diff_to:
+          id: diff_to
+          table: node_field_revision
+          field: diff_to
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: '0'
+          element_wrapper_class: ''
+          element_default_classes: false
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          entity_type: node
+          plugin_id: diff__to
+        changed:
+          id: changed
+          table: node_field_revision
+          field: changed
+          entity_type: node
+          entity_field: changed
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          plugin_id: field
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_alter_empty: true
+          click_sort_column: value
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        title:
+          id: title
+          table: node_field_revision
+          field: title
+          entity_type: node
+          entity_field: title
+          label: ''
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          settings:
+            link_to_entity: false
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          exclude: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      filters:
+        status:
+          value: true
+          table: node_field_revision
+          field: status
+          plugin_id: boolean
+          entity_type: node
+          entity_field: status
+          id: status
+          expose:
+            operator: ''
+          group: 1
+      sorts:
+        vid:
+          id: vid
+          table: node_field_revision
+          field: vid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: DESC
+          exposed: false
+          expose:
+            label: ''
+          entity_type: node
+          entity_field: vid
+          plugin_id: standard
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments:
+        nid:
+          id: nid
+          table: node_field_revision
+          field: nid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          default_action: empty
+          exception:
+            value: all
+            title_enable: false
+            title: All
+          title_enable: false
+          title: ''
+          default_argument_type: fixed
+          default_argument_options:
+            argument: ''
+          default_argument_skip_url: false
+          summary_options:
+            base_path: ''
+            count: true
+            items_per_page: 25
+            override: false
+          summary:
+            sort_order: asc
+            number_of_records: 0
+            format: default_summary
+          specify_validation: false
+          validate:
+            type: none
+            fail: 'not found'
+          validate_options: {  }
+          break_phrase: false
+          not: false
+          entity_type: node
+          entity_field: nid
+          plugin_id: node_nid
+      display_extenders: {  }
+      title: Revisions
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: node/%/diff-views
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
diff --git a/tests/modules/diff_test/diff_test.info.yml b/tests/modules/diff_test/diff_test.info.yml
index 939f946..4649315 100644
--- a/tests/modules/diff_test/diff_test.info.yml
+++ b/tests/modules/diff_test/diff_test.info.yml
@@ -4,6 +4,8 @@ dependencies:
   - diff
   - filter
   - editor
+  - node
+  - views
 hidden: true
 name: Diff tests
 package: diff
diff --git a/src/Plugin/views/field/DiffFrom.php b/src/Plugin/views/field/DiffFrom.php
new file mode 100644
index 0000000..629c408
--- /dev/null
+++ b/src/Plugin/views/field/DiffFrom.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\diff\Plugin\views\field\DiffFrom.
+ */
+
+namespace Drupal\diff\Plugin\views\field;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RedirectDestinationTrait;
+use Drupal\node\NodeInterface;
+use Drupal\views\Plugin\views\field\FieldPluginBase;
+
+/**
+ * @ViewsField("diff__from")
+ */
+class DiffFrom extends DiffPluginBase {
+
+  use RedirectDestinationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+
+    $options['label']['default'] = t('From');
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewsForm(&$form, FormStateInterface $form_state) {
+    // Replace the form submit button label.
+    $form['actions']['submit']['#value'] = $this->t('Compare');
+
+    $use_revision = array_key_exists('revision', $this->view->getQuery()->getEntityTableInfo());
+    if (!empty($this->view->result)) {
+      $form[$this->options['id']]['#tree'] = TRUE;
+      foreach ($this->view->result as $row_index => $row) {
+        $entity = $row->_entity;
+        $form[$this->options['id']][$row_index] = [
+          '#type' => 'radio',
+          '#parents' => [$this->options['id']],
+          '#title' => $this->t('Compare this item'),
+          '#title_display' => 'invisible',
+          '#return_value' => $this->calculateEntityDiffFormKey($entity, $use_revision),
+        ];
+      }
+    }
+  }
+
+  protected function getToFieldId() {
+    $to_fields = array_filter($this->view->field, function (FieldPluginBase $field) {
+      return $field instanceof DiffTo;
+    });
+    return array_keys($to_fields)[0];
+  }
+
+  /**
+   * Submit handler for the bulk form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+   *   Thrown when the user tried to access an action without access to it.
+   */
+  public function viewsFormSubmit(&$form, FormStateInterface $form_state) {
+    if ($form_state->get('step') == 'views_form_views_form') {
+      $diff_from = $form_state->getValue($this->options['id']);
+      $diff_from_entity = $this->loadEntityFromDiffFormKey($diff_from);
+
+      $diff_to = $form_state->getValue($this->getToFieldId());
+      $diff_to_entity = $this->loadEntityFromDiffFormKey($diff_to);
+
+      $options = array(
+        'query' => $this->getDestinationArray(),
+      );
+      $entity_type_id = $diff_from_entity->getEntityTypeId();
+
+
+      if ($diff_from_entity instanceof NodeInterface) {
+        $form_state->setRedirect('diff.revisions_diff', [$entity_type_id => $diff_from_entity->id(),'left_vid' => $diff_from_entity->getRevisionId(), 'right_vid' => $diff_to_entity->getRevisionId()], $options);
+      }
+      else {
+        $route_name = 'entity.' . $entity_type_id . '.revisions_diff';
+        $form_state->setRedirect($route_name, [$entity_type_id => $diff_from_entity->id(), 'left_revision' => $diff_from_entity->getRevisionId(), 'right_revision' => $diff_to_entity->getRevisionId()], $options);
+      }
+    }
+  }
+
+
+}
diff --git a/src/Plugin/views/field/DiffPluginBase.php b/src/Plugin/views/field/DiffPluginBase.php
new file mode 100644
index 0000000..8abd04b
--- /dev/null
+++ b/src/Plugin/views/field/DiffPluginBase.php
@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\diff\Plugin\views\field\DiffPluginBase.
+ */
+
+namespace Drupal\diff\Plugin\views\field;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\views\Plugin\views\field\FieldPluginBase;
+use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait;
+use Drupal\views\ResultRow;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class DiffPluginBase extends FieldPluginBase {
+
+  use UncacheableFieldHandlerTrait;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new BulkForm object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    $this->ensureMyTable();
+  }
+
+  public function getCacheMaxAge() {
+    return 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue(ResultRow $row, $field = NULL) {
+    return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
+  }
+
+  /**
+   * Calculates a diff form key.
+   *
+   * This generates a key that is used as the checkbox return value when
+   * submitting a diff form. This key allows the entity for the row to be loaded
+   * totally independently of the executed view row.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to calculate a diff form key for.
+   *
+   * @return string
+   *   The diff form key representing the entity's id, language and revision (if
+   *   applicable) as one string.
+   *
+   * @see self::loadEntityFromDiffFormKey()
+   */
+  protected function calculateEntityDiffFormKey(EntityInterface $entity) {
+    $key_parts = [$entity->language()->getId(), $entity->id()];
+
+    if ($entity instanceof RevisionableInterface) {
+      $key_parts[] = $entity->getRevisionId();
+    }
+
+    // An entity ID could be an arbitrary string (although they are typically
+    // numeric). JSON then Base64 encoding ensures the diff_form_key is
+    // safe to use in HTML, and that the key parts can be retrieved.
+    $key = json_encode($key_parts);
+    return base64_encode($key);
+  }
+
+
+  /**
+   * Loads an entity based on a diff form key.
+   *
+   * @param string $diff_form_key
+   *   The diff form key representing the entity's id, language and revision (if
+   *   applicable) as one string.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The entity loaded in the state (language, optionally revision) specified
+   *   as part of the diff form key.
+   */
+  protected function loadEntityFromDiffFormKey($diff_form_key) {
+    $key = base64_decode($diff_form_key);
+    $key_parts = json_decode($key);
+    $revision_id = NULL;
+
+    // If there are 3 items, vid will be last.
+    if (count($key_parts) === 3) {
+      $revision_id = array_pop($key_parts);
+    }
+
+    // The first two items will always be langcode and ID.
+    $id = array_pop($key_parts);
+    $langcode = array_pop($key_parts);
+
+    // Load the entity or a specific revision depending on the given key.
+    $storage = $this->entityTypeManager->getStorage($this->getEntityType());
+    $entity = $revision_id ? $storage->loadRevision($revision_id) : $storage->load($id);
+
+    if ($entity instanceof TranslatableInterface) {
+      $entity = $entity->getTranslation($langcode);
+    }
+
+    return $entity;
+  }
+
+}
diff --git a/src/Plugin/views/field/DiffTo.php b/src/Plugin/views/field/DiffTo.php
new file mode 100644
index 0000000..bbdb170
--- /dev/null
+++ b/src/Plugin/views/field/DiffTo.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\diff\Plugin\views\field\DiffFrom.
+ */
+
+namespace Drupal\diff\Plugin\views\field;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * @ViewsField("diff__to")
+ */
+class DiffTo extends DiffPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+
+    $options['label']['default'] = t('To');
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewsForm(&$form, FormStateInterface $form_state) {
+    $use_revision = array_key_exists('revision', $this->view->getQuery()->getEntityTableInfo());
+
+    if (!empty($this->view->result)) {
+      $form[$this->options['id']]['#tree'] = TRUE;
+      foreach ($this->view->result as $row_index => $row) {
+        $entity = $row->_entity;
+        $form[$this->options['id']][$row_index] = [
+          '#type' => 'radio',
+          '#parents' => [$this->options['id']],
+          '#title' => $this->t('Compare this item'),
+          '#title_display' => 'invisible',
+          '#return_value' => $this->calculateEntityDiffFormKey($entity, $use_revision),
+        ];
+      }
+    }
+  }
+
+}
diff --git a/src/Tests/DiffViewsTest.php b/src/Tests/DiffViewsTest.php
new file mode 100644
index 0000000..58dceb1
--- /dev/null
+++ b/src/Tests/DiffViewsTest.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\diff\Tests\DiffViewsTest.
+ */
+
+namespace Drupal\diff\Tests;
+
+use Drupal\Core\Url;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\views\Tests\ViewTestBase;
+
+/**
+ * Tests the diff views integration.
+ *
+ * @group diff
+ */
+class DiffViewsTest extends ViewTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'diff', 'user', 'diff_test'];
+
+  public function testDiffView() {
+    $node_type = NodeType::create([
+      'type' => 'article',
+      'label' => 'Article',
+    ]);
+    $node_type->save();
+    $node = Node::create([
+      'type' => 'article',
+      'title' => 'Test article: giraffe',
+    ]);
+    $node->save();
+
+    $node->setNewRevision(TRUE);
+    $node->setTitle('Test article: llama');
+    $node->save();
+
+    $this->drupalGet("node/{$node->id()}/diff-views");
+    $this->assertResponse(403);
+
+    $user = $this->createUser(['view all revisions']);
+    $this->drupalLogin($user);
+
+    $this->drupalGet("node/{$node->id()}/diff-views");
+    $this->assertResponse(200);
+
+    $from_first = (string) $this->cssSelect('#edit-diff-from--3')[0]->attributes()['value'];
+    $to_second = (string) $this->cssSelect('#edit-diff-to--2')[0]->attributes()['value'];
+
+    $edit = [
+      'diff_from' => $from_first,
+      'diff_to' => $to_second,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Compare'));
+    $this->assertUrl(Url::fromRoute('diff.revisions_diff', ['left_vid' => 1, 'right_vid' => 2]), ['query' => ['destination' => '/node/1/diff-views']]);
+    $this->assertRaw('<td class="diff-context diff-deletedline">Test article: <span class="diffchange">giraffe</span></td>');
+    $this->assertRaw('<td class="diff-context diff-addedline">Test article: <span class="diffchange">llama</span></td>');
+  }
+
+}
