diff --git a/book/src/configuration.md b/book/src/configuration.md
index 03948e2d..36e2fee2 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -65,6 +65,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
 | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
 | `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
 | `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
+| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
 | `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
 
 ### `[editor.statusline]` Section
diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs
index cc013d1e..e9fde476 100644
--- a/helix-term/src/commands/dap.rs
+++ b/helix-term/src/commands/dap.rs
@@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates};
 use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
 use helix_dap::{self as dap, Client};
 use helix_lsp::block_on;
-use helix_view::editor::Breakpoint;
+use helix_view::{editor::Breakpoint, graphics::Margin};
 
 use serde_json::{to_value, Value};
 use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -581,7 +581,12 @@ pub fn dap_variables(cx: &mut Context) {
     }
 
     let contents = Text::from(tui::text::Text::from(variables));
-    let popup = Popup::new("dap-variables", contents);
+    let margin = if cx.editor.popup_border() {
+        Margin::all(1)
+    } else {
+        Margin::none()
+    };
+    let popup = Popup::new("dap-variables", contents).margin(margin);
     cx.replace_or_push_layer("dap-variables", popup);
 }
 
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 87400461..ac6a1a21 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -23,6 +23,7 @@ use helix_core::{
 use helix_view::{
     document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
     editor::Action,
+    graphics::Margin,
     theme::Style,
     Document, View,
 };
@@ -744,7 +745,16 @@ pub fn code_action(cx: &mut Context) {
             });
             picker.move_down(); // pre-select the first item
 
-            let popup = Popup::new("code-action", picker).with_scrollbar(false);
+            let margin = if editor.menu_border() {
+                Margin::vertical(1)
+            } else {
+                Margin::none()
+            };
+
+            let popup = Popup::new("code-action", picker)
+                .with_scrollbar(false)
+                .margin(margin);
+
             compositor.replace_or_push("code-action", popup);
         };
 
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 1d4d6505..f530ce10 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -574,7 +574,6 @@ fn set_line_ending(
 
     Ok(())
 }
-
 fn earlier(
     cx: &mut compositor::Context,
     args: &[Cow<str>],
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index e0614722..7c6a0055 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
 use helix_view::{
     document::SavePoint,
     editor::CompleteAction,
+    graphics::Margin,
     theme::{Modifier, Style},
     ViewId,
 };
@@ -326,9 +327,18 @@ impl Completion {
                 }
             };
         });
+
+        let margin = if editor.menu_border() {
+            Margin::vertical(1)
+        } else {
+            Margin::none()
+        };
+
         let popup = Popup::new(Self::ID, menu)
             .with_scrollbar(false)
-            .ignore_escape_key(true);
+            .ignore_escape_key(true)
+            .margin(margin);
+
         let mut completion = Self {
             popup,
             start_offset,
@@ -569,6 +579,12 @@ impl Component for Completion {
         // clear area
         let background = cx.editor.theme.get("ui.popup");
         surface.clear_with(doc_area, background);
+
+        if cx.editor.popup_border() {
+            use tui::widgets::{Block, Borders, Widget};
+            Widget::render(Block::default().borders(Borders::ALL), doc_area, surface);
+        }
+
         markdown_doc.render(doc_area, surface, cx);
     }
 }
diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs
index 880df6d8..7037b155 100644
--- a/helix-term/src/ui/lsp.rs
+++ b/helix-term/src/ui/lsp.rs
@@ -92,7 +92,9 @@ impl Component for SignatureHelp {
             Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
         };
         let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
-        let sig_doc_area = area.clip_top(sig_text_area.height + 2);
+        let sig_doc_area = area
+            .clip_top(sig_text_area.height + 2)
+            .clip_bottom(u16::from(cx.editor.popup_border()));
         let sig_doc_para = Paragraph::new(sig_doc)
             .wrap(Wrap { trim: false })
             .scroll((cx.scroll.unwrap_or_default() as u16, 0));
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 9704b1f2..0ee64ce9 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -7,11 +7,18 @@ use crate::{
 use helix_core::fuzzy::MATCHER;
 use nucleo::pattern::{Atom, AtomKind, CaseMatching};
 use nucleo::{Config, Utf32Str};
-use tui::{buffer::Buffer as Surface, widgets::Table};
+use tui::{
+    buffer::Buffer as Surface,
+    widgets::{Block, Borders, Table, Widget},
+};
 
 pub use tui::widgets::{Cell, Row};
 
-use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
+use helix_view::{
+    editor::SmartTabConfig,
+    graphics::{Margin, Rect},
+    Editor,
+};
 use tui::layout::Constraint;
 
 pub trait Item: Sync + Send + 'static {
@@ -322,6 +329,15 @@ impl<T: Item + 'static> Component for Menu<T> {
         let selected = theme.get("ui.menu.selected");
         surface.clear_with(area, style);
 
+        let render_borders = cx.editor.menu_border();
+
+        let area = if render_borders {
+            Widget::render(Block::default().borders(Borders::ALL), area, surface);
+            area.inner(&Margin::vertical(1))
+        } else {
+            area
+        };
+
         let scroll = self.scroll;
 
         let options: Vec<_> = self
@@ -362,15 +378,19 @@ impl<T: Item + 'static> Component for Menu<T> {
             false,
         );
 
-        if let Some(cursor) = self.cursor {
-            let offset_from_top = cursor - scroll;
-            let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
-            left.set_style(selected);
-            let right = &mut surface[(
-                area.right().saturating_sub(1),
-                area.y + offset_from_top as u16,
-            )];
-            right.set_style(selected);
+        let render_borders = cx.editor.menu_border();
+
+        if !render_borders {
+            if let Some(cursor) = self.cursor {
+                let offset_from_top = cursor - scroll;
+                let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
+                left.set_style(selected);
+                let right = &mut surface[(
+                    area.right().saturating_sub(1),
+                    area.y + offset_from_top as u16,
+                )];
+                right.set_style(selected);
+            }
         }
 
         let fits = len <= win_height;
@@ -385,12 +405,13 @@ impl<T: Item + 'static> Component for Menu<T> {
             for i in 0..win_height {
                 cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
 
-                cell.set_symbol("▐"); // right half block
+                let half_block = if render_borders { "▌" } else { "▐" };
 
                 if scroll_line <= i && i < scroll_line + scroll_height {
                     // Draw scroll thumb
+                    cell.set_symbol(half_block);
                     cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
-                } else {
+                } else if !render_borders {
                     // Draw scroll track
                     cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
                 }
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index dff7b231..7a6ffe9d 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -3,7 +3,10 @@ use crate::{
     compositor::{Callback, Component, Context, Event, EventResult},
     ctrl, key,
 };
-use tui::buffer::Buffer as Surface;
+use tui::{
+    buffer::Buffer as Surface,
+    widgets::{Block, Borders, Widget},
+};
 
 use helix_core::Position;
 use helix_view::{
@@ -252,13 +255,29 @@ impl<T: Component> Component for Popup<T> {
         let background = cx.editor.theme.get("ui.popup");
         surface.clear_with(area, background);
 
-        let inner = area.inner(&self.margin);
+        let render_borders = cx.editor.popup_border();
+
+        let inner = if self
+            .contents
+            .type_name()
+            .starts_with("helix_term::ui::menu::Menu")
+        {
+            area
+        } else {
+            area.inner(&self.margin)
+        };
+
+        let border = usize::from(render_borders);
+        if render_borders {
+            Widget::render(Block::default().borders(Borders::ALL), area, surface);
+        }
+
         self.contents.render(inner, surface, cx);
 
         // render scrollbar if contents do not fit
         if self.has_scrollbar {
-            let win_height = inner.height as usize;
-            let len = self.child_size.1 as usize;
+            let win_height = (inner.height as usize).saturating_sub(2 * border);
+            let len = (self.child_size.1 as usize).saturating_sub(2 * border);
             let fits = len <= win_height;
             let scroll = self.scroll;
             let scroll_style = cx.editor.theme.get("ui.menu.scroll");
@@ -274,14 +293,15 @@ impl<T: Component> Component for Popup<T> {
 
                 let mut cell;
                 for i in 0..win_height {
-                    cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)];
+                    cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)];
 
-                    cell.set_symbol("▐"); // right half block
+                    let half_block = if render_borders { "▌" } else { "▐" };
 
                     if scroll_line <= i && i < scroll_line + scroll_height {
                         // Draw scroll thumb
+                        cell.set_symbol(half_block);
                         cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
-                    } else {
+                    } else if !render_borders {
                         // Draw scroll track
                         cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
                     }
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index f2e85307..76429a87 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -43,9 +43,8 @@ pub use helix_core::diagnostic::Severity;
 use helix_core::{
     auto_pairs::AutoPairs,
     syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap},
-    Change, LineEnding, NATIVE_LINE_ENDING,
+    Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
 };
-use helix_core::{Position, Selection};
 use helix_dap as dap;
 use helix_lsp::lsp;
 
@@ -291,6 +290,8 @@ pub struct Config {
     pub insert_final_newline: bool,
     /// Enables smart tab
     pub smart_tab: Option<SmartTabConfig>,
+    /// Draw border around popups.
+    pub popup_border: PopupBorderConfig,
     /// Which indent heuristic to use when a new line is inserted
     #[serde(default)]
     pub indent_heuristic: IndentationHeuristic,
@@ -797,6 +798,15 @@ impl From<LineEndingConfig> for LineEnding {
     }
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum PopupBorderConfig {
+    None,
+    All,
+    Popup,
+    Menu,
+}
+
 impl Default for Config {
     fn default() -> Self {
         Self {
@@ -844,6 +854,7 @@ impl Default for Config {
             default_line_ending: LineEndingConfig::default(),
             insert_final_newline: true,
             smart_tab: Some(SmartTabConfig::default()),
+            popup_border: PopupBorderConfig::None,
             indent_heuristic: IndentationHeuristic::default(),
         }
     }
@@ -1064,6 +1075,16 @@ impl Editor {
         }
     }
 
+    pub fn popup_border(&self) -> bool {
+        self.config().popup_border == PopupBorderConfig::All
+            || self.config().popup_border == PopupBorderConfig::Popup
+    }
+
+    pub fn menu_border(&self) -> bool {
+        self.config().popup_border == PopupBorderConfig::All
+            || self.config().popup_border == PopupBorderConfig::Menu
+    }
+
     pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
         motion(self);
         self.last_motion = Some(Box::new(motion));