diff --git a/Cargo.toml b/Cargo.toml
index f4f3f97f134..d8706089b66 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -168,7 +168,7 @@ string = ["clap_builder/string"]  # Allow runtime generated strings
 
 # In-work features
 unstable-v5 = ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecated"]
-unstable-ext = []
+unstable-ext = ["clap_builder/unstable-ext"]
 unstable-styles = ["clap_builder/unstable-styles"]  # deprecated
 
 [lib]
diff --git a/clap_complete/Cargo.toml b/clap_complete/Cargo.toml
index 495ba99ab98..c24e0749462 100644
--- a/clap_complete/Cargo.toml
+++ b/clap_complete/Cargo.toml
@@ -57,7 +57,7 @@ required-features = ["unstable-dynamic"]
 [features]
 default = []
 unstable-doc = ["unstable-dynamic"] # for docs.rs
-unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff"]
+unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
 debug = ["clap/debug"]
 
 [lints]
diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs
index 7a9015399a8..9d3e62f43c6 100644
--- a/clap_complete/src/dynamic/completer.rs
+++ b/clap_complete/src/dynamic/completer.rs
@@ -1,7 +1,9 @@
-use core::num;
+use std::any::type_name;
 use std::ffi::OsStr;
 use std::ffi::OsString;
+use std::sync::Arc;
 
+use clap::builder::ArgExt;
 use clap::builder::StyledStr;
 use clap_lex::OsStrExt as _;
 
@@ -198,7 +200,7 @@ fn complete_arg(
                                             comp.get_content().to_string_lossy()
                                         ))
                                         .help(comp.get_help().cloned())
-                                        .visible(comp.is_visible())
+                                        .hide(comp.is_hide_set())
                                     }),
                             );
                         }
@@ -241,7 +243,6 @@ fn complete_arg(
                                 comp.get_content().to_string_lossy()
                             ))
                             .help(comp.get_help().cloned())
-                            .visible(true)
                         }),
                 );
             } else if let Some(short) = arg.to_short() {
@@ -271,7 +272,7 @@ fn complete_arg(
                                         comp.get_content().to_string_lossy()
                                     ))
                                     .help(comp.get_help().cloned())
-                                    .visible(comp.is_visible())
+                                    .hide(comp.is_hide_set())
                                 }),
                         );
                     } else {
@@ -283,7 +284,7 @@ fn complete_arg(
                                     comp.get_content().to_string_lossy()
                                 ))
                                 .help(comp.get_help().cloned())
-                                .visible(comp.is_visible())
+                                .hide(comp.is_hide_set())
                             },
                         ));
                     }
@@ -324,8 +325,8 @@ fn complete_arg(
             }
         }
     }
-    if completions.iter().any(|a| a.is_visible()) {
-        completions.retain(|a| a.is_visible());
+    if completions.iter().any(|a| !a.is_hide_set()) {
+        completions.retain(|a| !a.is_hide_set());
     }
 
     Ok(completions)
@@ -346,10 +347,16 @@ fn complete_arg_value(
                 name.starts_with(value).then(|| {
                     CompletionCandidate::new(OsString::from(name))
                         .help(p.get_help().cloned())
-                        .visible(!p.is_hide_set())
+                        .hide(p.is_hide_set())
                 })
             }));
         }
+    } else if let Some(completer) = arg.get::<ArgValueCompleter>() {
+        let value_os = match value {
+            Ok(value) => OsStr::new(value),
+            Err(value_os) => value_os,
+        };
+        values.extend(complete_custom_arg_value(value_os, completer));
     } else {
         let value_os = match value {
             Ok(value) => OsStr::new(value),
@@ -386,6 +393,7 @@ fn complete_arg_value(
                 values.extend(complete_path(value_os, current_dir, |_| true));
             }
         }
+
         values.sort();
     }
 
@@ -428,20 +436,14 @@ fn complete_path(
             let path = entry.path();
             let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
             suggestion.push(""); // Ensure trailing `/`
-            completions.push(
-                CompletionCandidate::new(suggestion.as_os_str().to_owned())
-                    .help(None)
-                    .visible(true),
-            );
+            completions
+                .push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
         } else {
             let path = entry.path();
             if is_wanted(&path) {
                 let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
-                completions.push(
-                    CompletionCandidate::new(suggestion.as_os_str().to_owned())
-                        .help(None)
-                        .visible(true),
-                );
+                completions
+                    .push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
             }
         }
     }
@@ -449,6 +451,21 @@ fn complete_path(
     completions
 }
 
+fn complete_custom_arg_value(
+    value: &OsStr,
+    completer: &ArgValueCompleter,
+) -> Vec<CompletionCandidate> {
+    debug!("complete_custom_arg_value: completer={completer:?}, value={value:?}");
+
+    let mut values = Vec::new();
+    let custom_arg_values = completer.0.completions();
+    values.extend(custom_arg_values);
+
+    values.retain(|comp| comp.get_content().starts_with(&value.to_string_lossy()));
+
+    values
+}
+
 fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<CompletionCandidate> {
     debug!(
         "complete_subcommand: cmd={:?}, value={:?}",
@@ -476,7 +493,7 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
                 longs.into_iter().map(|s| {
                     CompletionCandidate::new(format!("--{}", s))
                         .help(a.get_help().cloned())
-                        .visible(!a.is_hide_set())
+                        .hide(a.is_hide_set())
                 })
             })
         })
@@ -494,7 +511,7 @@ fn hidden_longs_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
                 longs.into_iter().map(|s| {
                     CompletionCandidate::new(format!("--{}", s))
                         .help(a.get_help().cloned())
-                        .visible(false)
+                        .hide(true)
                 })
             })
         })
@@ -513,7 +530,7 @@ fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
                 shorts.into_iter().map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(a.get_help().cloned())
-                        .visible(!a.is_hide_set())
+                        .hide(a.is_hide_set())
                 })
             })
         })
@@ -546,12 +563,12 @@ fn subcommands(p: &clap::Command) -> Vec<CompletionCandidate> {
                 .map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(sc.get_about().cloned())
-                        .visible(!sc.is_hide_set())
+                        .hide(sc.is_hide_set())
                 })
                 .chain(sc.get_aliases().map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(sc.get_about().cloned())
-                        .visible(false)
+                        .hide(true)
                 }))
         })
         .collect()
@@ -667,8 +684,8 @@ pub struct CompletionCandidate {
     /// Help message with a completion candidate
     help: Option<StyledStr>,
 
-    /// Whether the completion candidate is visible
-    visible: bool,
+    /// Whether the completion candidate is hidden
+    hidden: bool,
 }
 
 impl CompletionCandidate {
@@ -688,8 +705,8 @@ impl CompletionCandidate {
     }
 
     /// Set the visibility of the completion candidate
-    pub fn visible(mut self, visible: bool) -> Self {
-        self.visible = visible;
+    pub fn hide(mut self, hidden: bool) -> Self {
+        self.hidden = hidden;
         self
     }
 
@@ -704,7 +721,65 @@ impl CompletionCandidate {
     }
 
     /// Get the visibility of the completion candidate
-    pub fn is_visible(&self) -> bool {
-        self.visible
+    pub fn is_hide_set(&self) -> bool {
+        self.hidden
     }
 }
+
+/// User-provided completion candidates for an argument.
+///
+/// This is useful when predefined value hints are not enough.
+pub trait CustomCompleter: Send + Sync {
+    /// All potential candidates for an argument.
+    ///
+    /// See [`CompletionCandidate`] for more information.
+    fn completions(&self) -> Vec<CompletionCandidate>;
+}
+
+impl<F> CustomCompleter for F
+where
+    F: Fn() -> Vec<CompletionCandidate> + Send + Sync,
+{
+    fn completions(&self) -> Vec<CompletionCandidate> {
+        self()
+    }
+}
+
+/// A wrapper for custom completer
+///
+/// # Example
+///
+/// ```rust
+/// use clap::Parser;
+/// use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate};
+///
+/// #[derive(Debug, Parser)]
+/// struct Cli {
+///     #[arg(long, add = ArgValueCompleter::new(|| { vec![
+///         CompletionCandidate::new("foo"),
+///         CompletionCandidate::new("bar"),
+///         CompletionCandidate::new("baz")] }))]
+///     custom: Option<String>,
+/// }
+///    
+/// ```
+#[derive(Clone)]
+pub struct ArgValueCompleter(Arc<dyn CustomCompleter>);
+
+impl ArgValueCompleter {
+    /// Create a new `ArgValueCompleter` with a custom completer
+    pub fn new<C: CustomCompleter>(completer: C) -> Self
+    where
+        C: 'static + CustomCompleter,
+    {
+        Self(Arc::new(completer))
+    }
+}
+
+impl std::fmt::Debug for ArgValueCompleter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(type_name::<Self>())
+    }
+}
+
+impl ArgExt for ArgValueCompleter {}
diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs
index 0c0fe22b720..31214fa1339 100644
--- a/clap_complete/tests/testsuite/dynamic.rs
+++ b/clap_complete/tests/testsuite/dynamic.rs
@@ -4,6 +4,7 @@ use std::fs;
 use std::path::Path;
 
 use clap::{builder::PossibleValue, Command};
+use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate, CustomCompleter};
 use snapbox::assert_data_eq;
 
 macro_rules! complete {
@@ -590,6 +591,37 @@ val3
     );
 }
 
+#[test]
+fn suggest_custom_arg_value() {
+    #[derive(Debug)]
+    struct MyCustomCompleter {}
+
+    impl CustomCompleter for MyCustomCompleter {
+        fn completions(&self) -> Vec<CompletionCandidate> {
+            vec![
+                CompletionCandidate::new("custom1"),
+                CompletionCandidate::new("custom2"),
+                CompletionCandidate::new("custom3"),
+            ]
+        }
+    }
+
+    let mut cmd = Command::new("dynamic").arg(
+        clap::Arg::new("custom")
+            .long("custom")
+            .add::<ArgValueCompleter>(ArgValueCompleter::new(MyCustomCompleter {})),
+    );
+
+    assert_data_eq!(
+        complete!(cmd, "--custom [TAB]"),
+        snapbox::str![
+            "custom1
+custom2
+custom3"
+        ],
+    );
+}
+
 #[test]
 fn suggest_multi_positional() {
     let mut cmd = Command::new("dynamic")