diff --git a/config/schema/flag.schema.yml b/config/schema/flag.schema.yml
index 6dfd05f..862c6e4 100644
--- a/config/schema/flag.schema.yml
+++ b/config/schema/flag.schema.yml
@@ -124,6 +124,9 @@ flag.link_type.plugin.confirm:
     flag_update_button:
       type: label
       label: 'Update flagging button text'
+    form_behavior:
+      type: string
+      label: 'Where should the form open (new page, modal, etc)'
 
 flag.link_type.plugin.field_entry:
   type: mapping
@@ -147,6 +150,9 @@ flag.link_type.plugin.field_entry:
     flag_update_button:
       type: label
       label: 'Update flagging button text'
+    form_behavior:
+      type: string
+      label: 'Where should the form open (new page, modal, etc)'
 
 action.configuration.flag_action:*.*:
   type: mapping
diff --git a/src/Plugin/ActionLink/FormEntryTypeBase.php b/src/Plugin/ActionLink/FormEntryTypeBase.php
index 3f87d1f..15c1480 100644
--- a/src/Plugin/ActionLink/FormEntryTypeBase.php
+++ b/src/Plugin/ActionLink/FormEntryTypeBase.php
@@ -2,8 +2,11 @@
 
 namespace Drupal\flag\Plugin\ActionLink;
 
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\flag\ActionLink\ActionLinkTypeBase;
+use Drupal\flag\FlagInterface;
 
 /**
  * Base class for link types using form entry.
@@ -21,6 +24,7 @@ abstract class FormEntryTypeBase extends ActionLinkTypeBase implements FormEntry
       'unflag_confirmation' => $this->t('Unflag this content?'),
       'flag_create_button' => $this->t('Create flagging'),
       'flag_delete_button' => $this->t('Delete flagging'),
+      'form_behavior' => 'default',
     ];
 
     return $options;
@@ -81,6 +85,18 @@ abstract class FormEntryTypeBase extends ActionLinkTypeBase implements FormEntry
       '#required' => TRUE,
     ];
 
+    $form['display']['settings']['link_options_' . $plugin_id]['form_behavior'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Form behavior'),
+      '#options' => [
+        'default' => $this->t('New page'),
+        'dialog' => $this->t('Dialog'),
+        'modal' => $this->t('Modal dialog'),
+      ],
+      '#description' => $this->t('If an option other than <em>new page</em> is selected, the form will open via AJAX on the same page.'),
+      '#default_value' => $this->configuration['form_behavior'],
+    ];
+
     return $form;
   }
 
@@ -113,6 +129,22 @@ abstract class FormEntryTypeBase extends ActionLinkTypeBase implements FormEntry
   /**
    * {@inheritdoc}
    */
+  public function getAsFlagLink(FlagInterface $flag, EntityInterface $entity) {
+    $render = parent::getAsFlagLink($flag, $entity);
+    if ($this->configuration['form_behavior'] !== 'default') {
+      $render['#attached']['library'][] = 'core/drupal.ajax';
+      $render['#attributes']['class'][] = 'use-ajax';
+      $render['#attributes']['data-dialog-type'] = $this->configuration['form_behavior'];
+      $render['#attributes']['data-dialog-options'] = Json::encode([
+        'width' => 'auto',
+      ]);
+    }
+    return $render;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getFlagQuestion() {
     return $this->configuration['flag_confirmation'];
   }
diff --git a/tests/src/FunctionalJavascript/ModalFormTest.php b/tests/src/FunctionalJavascript/ModalFormTest.php
new file mode 100644
index 0000000..181520f
--- /dev/null
+++ b/tests/src/FunctionalJavascript/ModalFormTest.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Drupal\Tests\flag\FunctionalJavascript;
+
+use Drupal\Core\Url;
+use Drupal\flag\Tests\FlagCreateTrait;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests modal form options for action link plugins.
+ *
+ * @group flag
+ */
+class ModalFormTest extends JavascriptTestBase {
+
+  use FlagCreateTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['flag', 'node', 'user'];
+
+  /**
+   * Flag to test with.
+   *
+   * @var \Drupal\flag\FlagInterface
+   */
+  protected $flag;
+
+  /**
+   * The flag service.
+   *
+   * @var \Drupal\flag\FlagServiceInterface
+   */
+  protected $flagService;
+
+  /**
+   * Test node.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $node;
+
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $admin;
+
+  /**
+   * Normal user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $webUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // A test flag.
+    $this->flag = $this->createFlag('node', [], 'confirm');
+    $this->flagService = $this->container->get('flag');
+
+    // A node to test with.
+    $this->admin = $this->createUser([], NULL, TRUE);
+    $type = $this->createContentType();
+    $this->node = $this->createNode([
+      'type' => $type->id(),
+      'uid' => $this->admin->id(),
+    ]);
+
+    $this->webUser = $this->createUser(array_keys($this->flag->actionPermissions()));
+    $this->drupalLogin($this->webUser);
+  }
+
+  /**
+   * Tests the modal form option for confirm and field entry link types.
+   */
+  public function testModalOption() {
+    // Verify default, non-modal behavior.
+    $this->drupalGet($this->node->toUrl());
+    $this->clickLink($this->flag->getFlagShortText());
+
+    // Should be on the confirm form page, since this isn't using a modal.
+    $expected = Url::fromRoute('flag.confirm_flag', [
+      'flag' => $this->flag->id(),
+      'entity_id' => $this->node->id(),
+    ]);
+    $this->assertSession()->addressEquals($expected->getInternalPath());
+    $this->assertSession()->buttonExists(t('Create flagging'))->press();
+    $this->assertSession()->addressEquals($this->node->toUrl());
+
+    // Unflag.
+    $this->clickLink($this->flag->getUnflagShortText());
+    $expected = Url::fromRoute('flag.confirm_unflag', [
+      'flag' => $this->flag->id(),
+      'entity_id' => $this->node->id(),
+    ]);
+    $this->assertSession()->addressEquals($expected->getInternalPath());
+    $this->assertSession()->buttonExists(t('Delete flagging'))->press();
+    $this->assertSession()->addressEquals($this->node->toUrl());
+
+    // Set the modal option for the 'confirm' link.
+    $configuration = $this->flag->getLinkTypePlugin()->getConfiguration();
+    $configuration['form_behavior'] = 'modal';
+    $this->flag->getLinkTypePlugin()->setConfiguration($configuration);
+    $this->flag->save();
+
+    $this->drupalGet($this->node->toUrl());
+    $this->clickLink($this->flag->getFlagShortText());
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    // Should still be on the node url, as this is using a modal.
+    $this->assertSession()->addressEquals($this->node->toUrl()
+      ->getInternalPath());
+    // Note, there is some odd behavior calling the `press()` method on the
+    // button, so after asserting it exists, click via this method.
+    $this->assertSession()->buttonExists(t('Create flagging'));
+    $this->click('button:contains("Create flagging")');
+    $this->assertSession()->addressEquals($this->node->toUrl()
+      ->getInternalPath());
+
+    // Unflag.
+    $this->clickLink($this->flag->getUnflagShortText());
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->addressEquals($this->node->toUrl()
+      ->getInternalPath());
+    $this->assertSession()->buttonExists(t('Delete flagging'));
+    $this->click('button:contains("Delete flagging")');
+    $this->assertSession()->addressEquals($this->node->toUrl()
+      ->getInternalPath());
+  }
+
+}
