diff --git a/examples.module b/examples.module
index 3e37702..d1836e2 100644
--- a/examples.module
+++ b/examples.module
@@ -50,6 +50,7 @@ function examples_toolbar() {
     'field_example' => 'field_example.description',
     'field_permission_example' => 'field_permission_example.description',
     'file_example' => 'file_example.fileapi',
+    'filter_example' => 'filter_example.page',
     'hooks_example' => 'hooks_example.description',
     'js_example' => 'js_example.info',
     'menu_example' => 'examples.menu_example',
diff --git a/filter_example/filter_example.info.yml b/filter_example/filter_example.info.yml
new file mode 100644
index 0000000..38380ab
--- /dev/null
+++ b/filter_example/filter_example.info.yml
@@ -0,0 +1,9 @@
+name: 'Filter example'
+type: module
+description: 'An example module showing how to define a custom filter.'
+package: 'Example modules'
+core: 8.x
+dependencies:
+  - drupal:node
+  - drupal:filter
+  - examples:examples
diff --git a/filter_example/filter_example.links.menu.yml b/filter_example/filter_example.links.menu.yml
new file mode 100644
index 0000000..9a05863
--- /dev/null
+++ b/filter_example/filter_example.links.menu.yml
@@ -0,0 +1,4 @@
+filter_example.page:
+  title: 'Filter Example'
+  route_name: 'filter_example.page'
+  expanded: 'TRUE'
diff --git a/filter_example/filter_example.module b/filter_example/filter_example.module
new file mode 100644
index 0000000..0d6a327
--- /dev/null
+++ b/filter_example/filter_example.module
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * @defgroup filter_example Example: Filter
+ * @ingroup examples
+ * @{
+ * Demonstrates the creation of filters.
+ *
+ * This is an example outlining how a module can be used to define a filter
+ * to be run on user-submitted content before it is output to the browser.
+ *
+ * To show all the capabilities of the filter system, we will define two filters
+ * in this module. One will substitute the string "foo" with an
+ * administratively-defined replacement string. The other will find a custom
+ * XML tag, <time />, and replace it by the current time.
+ *
+ * Foo filter
+ *
+ * Drupal has several content formats (they are not filters), and in our example
+ * the each filter can be configured differently for each of the text formats
+ * where it is enabled. This example filter provides configuration that will
+ * allow an administrator to determine what string should be replaced with the
+ * text "foo".
+ *
+ * Time filter.
+ *
+ * This filter is a little trickier to implement than the previous one.
+ * Since the input involves special HTML characters (< and >) we have to
+ * run the filter before HTML is escaped/stripped by other filters. But
+ * we want to use HTML in our result as well, and so if we run this filter
+ * first our replacement string could be escaped or stripped. The solution
+ * is to use the "prepare" operation to escape the special characters, and
+ * to later replace our escaped version in the "process" step.
+ *
+ * @see to \Drupal\filter\Plugin\FilterInterface
+ * @link https://www.drupal.org/docs/8/api/filter-api @endlink
+ */
diff --git a/filter_example/filter_example.routing.yml b/filter_example/filter_example.routing.yml
new file mode 100644
index 0000000..50658aa
--- /dev/null
+++ b/filter_example/filter_example.routing.yml
@@ -0,0 +1,7 @@
+filter_example.page:
+  path: '/examples/filter-example'
+  defaults:
+    _controller: '\Drupal\filter_example\Controller\FilterExampleController::description'
+    _title: 'Filter Example'
+  requirements:
+    _permission: 'access content'
diff --git a/filter_example/src/Controller/FilterExampleController.php b/filter_example/src/Controller/FilterExampleController.php
new file mode 100644
index 0000000..37442f2
--- /dev/null
+++ b/filter_example/src/Controller/FilterExampleController.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\filter_example\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\examples\Utility\DescriptionTemplateTrait;
+
+/**
+ * Controller routines for Filter example routes.
+ */
+class FilterExampleController extends ControllerBase {
+
+  use DescriptionTemplateTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getModuleName() {
+    return 'filter_example';
+  }
+
+}
diff --git a/filter_example/src/Plugin/Filter/FilterFoo.php b/filter_example/src/Plugin/Filter/FilterFoo.php
new file mode 100644
index 0000000..715a503
--- /dev/null
+++ b/filter_example/src/Plugin/Filter/FilterFoo.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\filter_example\Plugin\Filter;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+
+/**
+ * Foo filter.
+ *
+ * Drupal has several text formats (they are not filters), and in our example
+ * the foo replacement can be configured for each one of them, so the module
+ * includes a settingsForm method, with options to configure those
+ * replacements.
+ * Also, a Tips method will help showing the current replacement
+ * for the content type being edited.
+ *
+ * @Filter(
+ *   id = "filter_example_foo",
+ *   title = @Translation("Foo Filter (example)"),
+ *   description = @Translation("Every instance of 'foo' in the input text will be replaced with a configurable replacement."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE,
+ *   settings = {
+ *     "filter_example_foo" = "bar"
+ *   }
+ * )
+ * @see \Drupal\filter\Annotation\Filter
+ * @see \Drupal\filter\Plugin\FilterInterface
+ */
+class FilterFoo extends FilterBase {
+
+  /**
+   * Settings for foo filter.
+   *
+   * Make use of settings to have different replacements for every input format.
+   * Since we allow the administrator to define the string that gets substituted
+   * when "foo" is encountered, we need to provide an interface for this kind of
+   * customization.
+   *
+   * The settings defined in this form are stored in the database by the filter
+   * module, and they will be available in the $this->settings.
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $form['filter_example_foo'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Substitution string'),
+      '#default_value' => $this->settings['filter_example_foo'],
+      '#description' => $this->t('The string to substitute for "foo" everywhere in the text.'),
+    ];
+    return $form;
+  }
+
+  /**
+   * Foo filter process callback.
+   *
+   * The actual filtering is performed here. The supplied text should be
+   * returned once any necessary substitutions have taken place.
+   * The example just replaces foo with our custom defined string in
+   * the settings page.
+   */
+  public function process($text, $langcode) {
+    $replace = $this->settings['filter_example_foo'];
+    $new_text = str_replace('foo', $replace, $text);
+    return new FilterProcessResult($new_text);
+  }
+
+  /**
+   * Filter tips callback for foo filter.
+   *
+   * The tips callback allows filters to provide help text to users during the
+   * content editing process. Short tips are provided on the content editing
+   * screen, while long tips are provided on a separate linked page. Short tips
+   * are optional, but long tips are highly recommended.
+   */
+  public function tips($long = FALSE) {
+    $replacement = $this->settings['filter_example_foo'];
+    if (!$long) {
+      // This string will be shown in the content add/edit form.
+      return $this->t('%foo replaced with %replacement.', ['%foo' => 'foo', '%replacement' => $replacement]);
+    }
+    else {
+      return $this->t('Every instance of "foo" in the input text will be replaced with a configurable value. You can configure this value and put whatever you want there. The replacement value is "%replacement".', ['%replacement' => $replacement]);
+    }
+  }
+
+}
diff --git a/filter_example/src/Plugin/Filter/FilterTime.php b/filter_example/src/Plugin/Filter/FilterTime.php
new file mode 100644
index 0000000..7efc4aa
--- /dev/null
+++ b/filter_example/src/Plugin/Filter/FilterTime.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\filter_example\Plugin\Filter;
+
+use Drupal\filter\Plugin\FilterBase;
+use Drupal\filter\FilterProcessResult;
+
+/**
+ * Provides a filter to replace "foo".
+ *
+ * When used in combination with the filter_align filter, this must run last.
+ *
+ * @Filter(
+ *   id = "filter_example_time",
+ *   title = @Translation("Time Tag (example)"),
+ *   description = @Translation("Every instance of the special &lt;time /&gt; tag will be replaced with the current date and time in the user's specified time zone."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE,
+ * )
+ * @see \Drupal\filter\Annotation\Filter
+ * @see \Drupal\filter\Plugin\FilterInterface
+ */
+class FilterTime extends FilterBase {
+
+  /**
+   * Time filter prepare callback.
+   *
+   * We'll use [filter-example-time] as a replacement for the time tag.
+   * Note that in a more complicated filter a closing tag may also be
+   * required.
+   */
+  public function prepare($text, $langcode) {
+    return preg_replace('!<time ?/>!', '[filter-example-time]', $text);
+  }
+
+  /**
+   * Time filter process callback.
+   *
+   * Now, in the "process" step, we'll search for our escaped time tags and
+   * do the real filtering: replace the xml tag with the date.
+   */
+  public function process($text, $langcode) {
+    $new_text = str_replace('[filter-example-time]', '<em>' . format_date(time()) . '</em>', $text);
+    return new FilterProcessResult($new_text);
+  }
+
+  /**
+   * Filter tips callback for time filter.
+   *
+   * The tips callback allows filters to provide help text to users during the
+   * content editing process. Short tips are provided on the content editing
+   * screen, while long tips are provided on a separate linked page. Short tips
+   * are optional, but long tips are highly recommended.
+   */
+  public function tips($long = FALSE) {
+    return $this->t('<em>&lt;time /&gt;</em> is replaced with the current time.');
+  }
+
+}
diff --git a/filter_example/templates/description.html.twig b/filter_example/templates/description.html.twig
new file mode 100644
index 0000000..e54cac8
--- /dev/null
+++ b/filter_example/templates/description.html.twig
@@ -0,0 +1,20 @@
+{#
+/**
+ * @file
+ * Contains the text of the filter_example explanation page
+ */
+#}
+
+{% set filter_admin_overview = path('filter.admin_overview') %}
+
+{% trans %}
+<p>This example provides two filters.</p>
+<p>Foo Filter replaces "foo" with a configurable replacement.</p>
+<p>Time Tag replaces the string "&lt;time /&gt;" with the current time.</p>
+<p>
+To use these filters, go to <a href="{{ filter_admin_overview }}">admin/config/content/formats</a> and edit whichever
+text format you're using to include these filters. You can include these filters by clicking on the checkboxes next to
+each of them. Make sure Time filter happens before 'Limit HTML tags' if not the "&lt;time /&gt;" tag will be removed before
+being replaced.
+</p>
+{% endtrans %}
diff --git a/filter_example/tests/filter_example_test/config/install/filter.format.filtered_html.yml b/filter_example/tests/filter_example_test/config/install/filter.format.filtered_html.yml
new file mode 100644
index 0000000..ba403db
--- /dev/null
+++ b/filter_example/tests/filter_example_test/config/install/filter.format.filtered_html.yml
@@ -0,0 +1,15 @@
+format: filtered_html
+name: 'Filtered HTML'
+weight: 1
+filters:
+  filter_url:
+    id: filter_url
+    provider: filter
+    weight: -1
+    status: true
+  filter_html:
+    id: filter_html
+    provider: filter
+    status: true
+    settings:
+      allowed_html: '<p> <br> <strong> <a href hreflang>'
diff --git a/filter_example/tests/filter_example_test/config/install/filter.format.full_html.yml b/filter_example/tests/filter_example_test/config/install/filter.format.full_html.yml
new file mode 100644
index 0000000..abb5662
--- /dev/null
+++ b/filter_example/tests/filter_example_test/config/install/filter.format.full_html.yml
@@ -0,0 +1,3 @@
+format: full_html
+name: 'Full HTML'
+weight: 2
diff --git a/filter_example/tests/filter_example_test/filter_example_test.info.yml b/filter_example/tests/filter_example_test/filter_example_test.info.yml
new file mode 100644
index 0000000..4ea941b
--- /dev/null
+++ b/filter_example/tests/filter_example_test/filter_example_test.info.yml
@@ -0,0 +1,7 @@
+name: 'Filter test module'
+type: module
+description: 'Tests filter hooks and functions.'
+package: Testing
+core: 8.x
+dependencies:
+  - drupal:filter
diff --git a/filter_example/tests/src/Functional/FilterExampleTest.php b/filter_example/tests/src/Functional/FilterExampleTest.php
new file mode 100644
index 0000000..2efda87
--- /dev/null
+++ b/filter_example/tests/src/Functional/FilterExampleTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\Tests\filter_example\Functional;
+
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Test the functionality of the Filter Example module.
+ *
+ * @ingroup filter_example
+ * @group examples
+ */
+class FilterExampleTest extends BrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['filter_example_test', 'filter_example'];
+
+  /**
+   * A user with administrative permissions.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+  protected $filteredHtml;
+  protected $fullHtml;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->filteredHtml = FilterFormat::load('filtered_html');
+    $this->fullHtml = FilterFormat::load('full_html');
+    // Create users.
+    $this->adminUser = $this->drupalCreateUser([
+      'administer filters',
+      $this->filteredHtml->getPermissionName(),
+      $this->fullHtml->getPermissionName(),
+      'bypass node access',
+    ]);
+  }
+
+  /**
+   * Functional test of the foo filter.
+   *
+   * Login user, create an example node, and test filter functionality through
+   * the admin user interfaces.
+   */
+  public function testFilterExampleBasic() {
+    $this->drupalLogin($this->adminUser);
+    $web_assert = $this->assertSession();
+
+    $edit = [
+      'filters[filter_example_time][status]' => TRUE,
+      'filters[filter_example_foo][status]' => TRUE,
+    ];
+    $this->drupalPostForm('admin/config/content/formats/manage/' . $this->filteredHtml->id(), $edit, t('Save configuration'));
+    $web_assert->pageTextContains(t('The text format ' . $this->filteredHtml->label() . ' has been updated.'));
+
+    $content_type = $this->drupalCreateContentType();
+
+    $edit = [];
+    $edit['title[0][value]'] = $this->randomMachineName();
+    $edit['body[0][value]'] = 'What foo is it? it is <time />';
+    $edit['body[0][format]'] = $this->filteredHtml->id();
+
+    $this->drupalPostForm('node/add/' . $content_type->id(), $edit, t('Save'));
+    $web_assert->statusCodeEquals(200);
+    $time = \Drupal::service('date.formatter')->format(time());
+    $node = $this->drupalGetNodeByTitle($edit['title[0][value]'], TRUE);
+
+    $web_assert->pageTextContains(t('What bar is it? it is ' . $time));
+    $this->drupalGet('node/' . $node->id() . '/edit');
+
+    // Enable foo filter in other format id 2.
+    $edit = [
+      'filters[filter_example_foo][status]' => TRUE,
+    ];
+    $this->drupalPostForm('admin/config/content/formats/manage/' . $this->fullHtml->id(), $edit, t('Save configuration'));
+    $web_assert->pageTextContains(t('The text format ' . $this->fullHtml->label() . ' has been updated.'));
+
+    $replacement = $this->randomMachineName();
+    $options = [
+      'filters[filter_example_foo][settings][filter_example_foo]' => $replacement,
+    ];
+    $this->drupalPostForm('admin/config/content/formats/manage/' . $this->fullHtml->id(), $options, t('Save configuration'));
+    $web_assert->pageTextContains(t('The text format ' . $this->fullHtml->label() . ' has been updated.'));
+    $edit = [];
+    $edit['title[0][value]'] = $this->randomMachineName();
+    $edit['body[0][value]'] = 'What foo is it? it is <time />';
+    $edit['body[0][format]'] = $this->fullHtml->id();
+
+    $this->drupalPostForm('node/add/' . $content_type->id(), $edit, t('Save'));
+    $web_assert->statusCodeEquals(200);
+
+    $web_assert->pageTextContains('What ' . $replacement . ' is it');
+
+  }
+
+}
