diff --git a/Cargo.toml b/Cargo.toml
index 0268d36..9ae3d94 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,7 +30,7 @@ serde_ignored = "0.1.7"
 serde_json = "1.0.91"
 siphasher = "0.3.10"
 strsim = "0.10.0"
-structopt = "0.3.26"
+clap = { version = "4.2.5", features = ["derive"] }
 toml = "0.5.11"
 ureq = { version = "2.6.2", features = ["json"] }
 walkdir = "2.3.2"
diff --git a/src/command/build.rs b/src/command/build.rs
index c557212..ad50e08 100644
--- a/src/command/build.rs
+++ b/src/command/build.rs
@@ -19,7 +19,7 @@ use std::fmt;
 use std::path::PathBuf;
 use std::str::FromStr;
 use std::time::Instant;
-use structopt::clap::AppSettings;
+use clap::{Args};
 
 /// Everything required to configure and run the `wasm-pack build` command.
 #[allow(missing_docs)]
@@ -109,74 +109,67 @@ pub enum BuildProfile {
 }
 
 /// Everything required to configure and run the `wasm-pack build` command.
-#[derive(Debug, StructOpt)]
-#[structopt(
-    // Allows unknown `--option`s to be parsed as positional arguments, so we can forward it to `cargo`.
-    setting = AppSettings::AllowLeadingHyphen,
-
-    // Allows `--` to be parsed as an argument, so we can forward it to `cargo`.
-    setting = AppSettings::TrailingVarArg,
-)]
+#[derive(Debug, Args)]
+#[command(allow_hyphen_values = true, trailing_var_arg = true)]
 pub struct BuildOptions {
     /// The path to the Rust crate. If not set, searches up the path from the current directory.
-    #[structopt(parse(from_os_str))]
+    #[clap()]
     pub path: Option<PathBuf>,
 
     /// The npm scope to use in package.json, if any.
-    #[structopt(long = "scope", short = "s")]
+    #[clap(long = "scope", short = 's')]
     pub scope: Option<String>,
 
-    #[structopt(long = "mode", short = "m", default_value = "normal")]
+    #[clap(long = "mode", short = 'm', default_value = "normal")]
     /// Sets steps to be run. [possible values: no-install, normal, force]
     pub mode: InstallMode,
 
-    #[structopt(long = "no-typescript")]
+    #[clap(long = "no-typescript")]
     /// By default a *.d.ts file is generated for the generated JS file, but
     /// this flag will disable generating this TypeScript file.
     pub disable_dts: bool,
 
-    #[structopt(long = "weak-refs")]
+    #[clap(long = "weak-refs")]
     /// Enable usage of the JS weak references proposal.
     pub weak_refs: bool,
 
-    #[structopt(long = "reference-types")]
+    #[clap(long = "reference-types")]
     /// Enable usage of WebAssembly reference types.
     pub reference_types: bool,
 
-    #[structopt(long = "target", short = "t", default_value = "bundler")]
+    #[clap(long = "target", short = 't', default_value = "bundler")]
     /// Sets the target environment. [possible values: bundler, nodejs, web, no-modules]
     pub target: Target,
 
-    #[structopt(long = "debug")]
+    #[clap(long = "debug")]
     /// Deprecated. Renamed to `--dev`.
     pub debug: bool,
 
-    #[structopt(long = "dev")]
+    #[clap(long = "dev")]
     /// Create a development build. Enable debug info, and disable
     /// optimizations.
     pub dev: bool,
 
-    #[structopt(long = "release")]
+    #[clap(long = "release")]
     /// Create a release build. Enable optimizations and disable debug info.
     pub release: bool,
 
-    #[structopt(long = "profiling")]
+    #[clap(long = "profiling")]
     /// Create a profiling build. Enable optimizations and debug info.
     pub profiling: bool,
 
-    #[structopt(long = "out-dir", short = "d", default_value = "pkg")]
+    #[clap(long = "out-dir", short = 'd', default_value = "pkg")]
     /// Sets the output directory with a relative path.
     pub out_dir: String,
 
-    #[structopt(long = "out-name")]
+    #[clap(long = "out-name")]
     /// Sets the output file names. Defaults to package name.
     pub out_name: Option<String>,
 
-    #[structopt(long = "no-pack", alias = "no-package")]
+    #[clap(long = "no-pack", alias = "no-package")]
     /// Option to not generate a package.json
     pub no_pack: bool,
 
-    #[structopt(allow_hyphen_values = true)]
     /// List of extra options to pass to `cargo build`
     pub extra_options: Vec<String>,
 }
@@ -225,7 +218,7 @@ impl Build {
             (false, false, false) | (false, true, false) => BuildProfile::Release,
             (true, false, false) => BuildProfile::Dev,
             (false, false, true) => BuildProfile::Profiling,
-            // Unfortunately, `structopt` doesn't expose clap's `conflicts_with`
+            // Unfortunately, `clap` doesn't expose clap's `conflicts_with`
             // functionality yet, so we have to implement it ourselves.
             _ => bail!("Can only supply one of the --dev, --release, or --profiling flags"),
         };
diff --git a/src/command/mod.rs b/src/command/mod.rs
index 631eca7..dad39f4 100644
--- a/src/command/mod.rs
+++ b/src/command/mod.rs
@@ -20,64 +20,64 @@ use crate::install::InstallMode;
 use anyhow::Result;
 use log::info;
 use std::path::PathBuf;
-
+use clap::Subcommand;
+use clap::builder::ValueParser;
 /// The various kinds of commands that `wasm-pack` can execute.
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Subcommand)]
 pub enum Command {
     /// 🏗️  build your npm package!
-    #[structopt(name = "build", alias = "init")]
+    #[clap(name = "build", alias = "init")]
     Build(BuildOptions),
 
-    #[structopt(name = "pack")]
+    #[clap(name = "pack")]
     /// 🍱  create a tar of your npm package but don't publish!
     Pack {
         /// The path to the Rust crate. If not set, searches up the path from the current directory.
-        #[structopt(parse(from_os_str))]
+        #[clap(value_parser = ValueParser::os_string())]
         path: Option<PathBuf>,
     },
 
-    #[structopt(name = "new")]
+    #[clap(name = "new")]
     /// 🐑 create a new project with a template
     Generate {
         /// The name of the project
         name: String,
         /// The URL to the template
-        #[structopt(
+        #[clap(
             long = "template",
-            short = "temp",
             default_value = "https://github.com/rustwasm/wasm-pack-template"
         )]
         template: String,
-        #[structopt(long = "mode", short = "m", default_value = "normal")]
+        #[clap(long = "mode", short = 'm', default_value = "normal")]
         /// Should we install or check the presence of binary tools. [possible values: no-install, normal, force]
         mode: InstallMode,
     },
 
-    #[structopt(name = "publish")]
+    #[clap(name = "publish")]
     /// 🎆  pack up your npm package and publish!
     Publish {
-        #[structopt(long = "target", short = "t", default_value = "bundler")]
+        #[clap(long = "target", short = 't', default_value = "bundler")]
         /// Sets the target environment. [possible values: bundler, nodejs, web, no-modules]
         target: String,
 
         /// The access level for the package to be published
-        #[structopt(long = "access", short = "a")]
+        #[clap(long = "access", short = 'a')]
         access: Option<Access>,
 
         /// The distribution tag being used for publishing.
         /// See https://docs.npmjs.com/cli/dist-tag
-        #[structopt(long = "tag")]
+        #[clap(long = "tag")]
         tag: Option<String>,
 
         /// The path to the Rust crate. If not set, searches up the path from the current directory.
-        #[structopt(parse(from_os_str))]
+        #[clap(value_parser = ValueParser::os_string())]
         path: Option<PathBuf>,
     },
 
-    #[structopt(name = "login", alias = "adduser", alias = "add-user")]
+    #[clap(name = "login", alias = "adduser", alias = "add-user")]
     /// 👤  Add an npm registry user account! (aliases: adduser, add-user)
     Login {
-        #[structopt(long = "registry", short = "r")]
+        #[clap(long = "registry", short = 'r')]
         /// Default: 'https://registry.npmjs.org/'.
         /// The base URL of the npm package registry. If scope is also
         /// specified, this registry will only be used for packages with that
@@ -85,13 +85,13 @@ pub enum Command {
         /// currently in, if any.
         registry: Option<String>,
 
-        #[structopt(long = "scope", short = "s")]
+        #[clap(long = "scope", short = 's')]
         /// Default: none.
         /// If specified, the user and login credentials given will be
         /// associated with the specified scope.
         scope: Option<String>,
 
-        #[structopt(long = "auth-type", short = "t")]
+        #[clap(long = "auth-type", short = 't')]
         /// Default: 'legacy'.
         /// Type: 'legacy', 'sso', 'saml', 'oauth'.
         /// What authentication strategy to use with adduser/login. Some npm
@@ -100,7 +100,7 @@ pub enum Command {
         auth_type: Option<String>,
     },
 
-    #[structopt(name = "test")]
+    #[clap(name = "test")]
     /// 👩‍🔬  test your wasm!
     Test(TestOptions),
 }
diff --git a/src/command/publish/access.rs b/src/command/publish/access.rs
index 6b374b4..1cbfea6 100644
--- a/src/command/publish/access.rs
+++ b/src/command/publish/access.rs
@@ -3,7 +3,7 @@ use std::fmt;
 use std::str::FromStr;
 
 /// Represents access level for the to-be publish package. Passed to `wasm-pack publish` as a flag, e.g. `--access=public`.
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub enum Access {
     /// Access is granted to all. All unscoped packages *must* be public.
     Public,
diff --git a/src/command/test.rs b/src/command/test.rs
index d7b6642..c093974 100644
--- a/src/command/test.rs
+++ b/src/command/test.rs
@@ -14,68 +14,63 @@ use log::info;
 use std::path::PathBuf;
 use std::str::FromStr;
 use std::time::Instant;
-use structopt::clap::AppSettings;
+use clap::Args;
+use clap::builder::ValueParser;
 
-#[derive(Debug, Default, StructOpt)]
-#[structopt(
-    // Allows unknown `--option`s to be parsed as positional arguments, so we can forward it to `cargo`.
-    setting = AppSettings::AllowLeadingHyphen,
-
-    // Allows `--` to be parsed as an argument, so we can forward it to `cargo`.
-    setting = AppSettings::TrailingVarArg,
-)]
+#[derive(Debug, Default, Args)]
+#[command(allow_hyphen_values = true, trailing_var_arg = true)]
 /// Everything required to configure the `wasm-pack test` command.
 pub struct TestOptions {
-    #[structopt(long = "node")]
+    #[clap(long = "node")]
     /// Run the tests in Node.js.
     pub node: bool,
 
-    #[structopt(long = "firefox")]
+    #[clap(long = "firefox")]
     /// Run the tests in Firefox. This machine must have a Firefox installation.
     /// If the `geckodriver` WebDriver client is not on the `$PATH`, and not
     /// specified with `--geckodriver`, then `wasm-pack` will download a local
     /// copy.
     pub firefox: bool,
 
-    #[structopt(long = "geckodriver", parse(from_os_str))]
+    #[clap(long = "geckodriver", value_parser = ValueParser::os_string())]
     /// The path to the `geckodriver` WebDriver client for testing in
     /// Firefox. Implies `--firefox`.
     pub geckodriver: Option<PathBuf>,
 
-    #[structopt(long = "chrome")]
+    #[clap(long = "chrome")]
     /// Run the tests in Chrome. This machine must have a Chrome installation.
     /// If the `chromedriver` WebDriver client is not on the `$PATH`, and not
     /// specified with `--chromedriver`, then `wasm-pack` will download a local
     /// copy.
     pub chrome: bool,
 
-    #[structopt(long = "chromedriver", parse(from_os_str))]
+    #[clap(long = "chromedriver", value_parser = ValueParser::os_string())]
     /// The path to the `chromedriver` WebDriver client for testing in
     /// Chrome. Implies `--chrome`.
     pub chromedriver: Option<PathBuf>,
 
-    #[structopt(long = "safari")]
+    #[clap(long = "safari")]
     /// Run the tests in Safari. This machine must have a Safari installation,
     /// and the `safaridriver` WebDriver client must either be on the `$PATH` or
     /// specified explicitly with the `--safaridriver` flag. `wasm-pack` cannot
     /// download the `safaridriver` WebDriver client for you.
     pub safari: bool,
 
-    #[structopt(long = "safaridriver", parse(from_os_str))]
+    #[clap(long = "safaridriver", value_parser = ValueParser::os_string())]
     /// The path to the `safaridriver` WebDriver client for testing in
     /// Safari. Implies `--safari`.
     pub safaridriver: Option<PathBuf>,
 
-    #[structopt(long = "headless")]
+    #[clap(long = "headless")]
     /// When running browser tests, run the browser in headless mode without any
     /// UI or windows.
     pub headless: bool,
 
-    #[structopt(long = "mode", short = "m", default_value = "normal")]
+    #[clap(long = "mode", short = 'm', default_value = "normal")]
     /// Sets steps to be run. [possible values: no-install, normal]
     pub mode: InstallMode,
 
-    #[structopt(long = "release", short = "r")]
+    #[clap(long = "release", short = 'r')]
     /// Build with the release profile.
     pub release: bool,
 
@@ -85,7 +80,6 @@ pub struct TestOptions {
     ///
     /// This is a workaround to allow wasm pack to provide the same command line interface as `cargo`.
     /// See <https://github.com/rustwasm/wasm-pack/pull/851> for more information.
-    #[structopt(allow_hyphen_values = true)]
     pub path_and_extra_options: Vec<String>,
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index 1f5c508..af66554 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -15,8 +15,6 @@ extern crate which;
 extern crate serde_derive;
 extern crate serde_ignored;
 extern crate serde_json;
-#[macro_use]
-extern crate structopt;
 extern crate binary_install;
 extern crate chrono;
 extern crate dialoguer;
@@ -43,27 +41,29 @@ pub mod target;
 pub mod test;
 pub mod wasm_opt;
 
+use clap::Parser;
+use clap::builder::ArgAction;
 use crate::progressbar::{LogLevel, ProgressOutput};
 
 /// The global progress bar and user-facing message output.
 pub static PBAR: ProgressOutput = ProgressOutput::new();
 
 /// 📦 ✨  pack and publish your wasm!
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub struct Cli {
     /// The subcommand to run.
-    #[structopt(subcommand)] // Note that we mark a field as a subcommand
+    #[clap(subcommand)] // Note that we mark a field as a subcommand
     pub cmd: command::Command,
 
     /// Log verbosity is based off the number of v used
-    #[structopt(long = "verbose", short = "v", parse(from_occurrences))]
+    #[clap(long = "verbose", short = 'v', action = ArgAction::Count)]
     pub verbosity: u8,
 
-    #[structopt(long = "quiet", short = "q")]
+    #[clap(long = "quiet", short = 'q')]
     /// No output printed to stdout
     pub quiet: bool,
 
-    #[structopt(long = "log-level", default_value = "info")]
+    #[clap(long = "log-level", default_value = "info")]
     /// The maximum level of messages that should be logged by wasm-pack. [possible values: info, warn, error]
     pub log_level: LogLevel,
 }
diff --git a/src/main.rs b/src/main.rs
index c77f845..048561a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,7 @@ extern crate atty;
 extern crate env_logger;
 extern crate human_panic;
 extern crate log;
-extern crate structopt;
+extern crate clap;
 extern crate wasm_pack;
 extern crate which;
 
@@ -14,7 +14,7 @@ use std::env;
 use std::panic;
 use std::sync::mpsc;
 use std::thread;
-use structopt::StructOpt;
+use clap::Parser;
 use wasm_pack::{
     build::{self, WasmPackVersion},
     command::run_wasm_pack,
@@ -79,7 +79,7 @@ fn run() -> Result<()> {
         }
     }
 
-    let args = Cli::from_args();
+    let args = Cli::parse();
 
     PBAR.set_log_level(args.log_level);
 
diff --git a/tests/all/main.rs b/tests/all/main.rs
index c99a871..a7cdb8b 100644
--- a/tests/all/main.rs
+++ b/tests/all/main.rs
@@ -8,7 +8,7 @@ extern crate binary_install;
 extern crate serde_json;
 #[macro_use]
 extern crate serial_test;
-extern crate structopt;
+extern crate clap;
 extern crate tempfile;
 extern crate wasm_pack;