Building the Perfect Neovim Setup for React & Next.js Development

My Neovim Settings for Fullstack development.

ByEdward J Tan
26 min read

The Ultimate Neovim Setup for React & Next.js Development

Introduction

Why Neovim?

Neovim offers a pure keyboard-driven workflow that maximizes efficiency without mouse dependency, while being highly customizable to tailor every aspect to your development style. Its lightweight nature ensures fast startup and minimal resource usage, making it perfect for both local development and remote work. The configuration is completely portable, allowing you to easily transfer all your settings when SSH-ing to servers or switching machines. Being terminal-native, it integrates seamlessly into any development environment and maintains consistency across different systems.

About This Setup

  • Optimized specifically for web development with React/Next.js
  • Clean, minimal configuration without AI plugins (I use Claude Code as my AI copilot)
  • Battle-tested on real projects
  • Configuration Repository: GitHub setup
Video demo of developing Next.js Application

Environment & Foundation

Installing Neovim on Arch Linux

HLJS BASH
# Install Neovim
sudo pacman -S neovim

# Install LazyVim (requires git and a Nerd Font)
sudo pacman -S git
# Install a Nerd Font (example: JetBrains Mono)
yay -S ttf-jetbrains-mono-nerd

Setting up LazyVim

HLJS BASH
# Backup existing config (if any)
mv ~/.config/nvim ~/.config/nvim.bak

# Clone LazyVim starter
git clone https://github.com/LazyVim/starter ~/.config/nvim

# Remove .git folder to make it your own
rm -rf ~/.config/nvim/.git
  • System: Arch Linux setup considerations
  • Neovim Distribution: LazyVim as the solid foundation
  • Configuration Repository: Your GitHub setup

Core Development Plugins

1. Language Server & Autocompletion

nvim-cmp provides VSCode-like intelligent autocompletion that understands React components, props, and TypeScript interfaces. Combined with lspconfig.nvim, you get instant feedback on TypeScript errors, auto-imports, and method signatures. The setup includes React-specific completions for JSX elements, component props, and Next.js API routes.

nvim-cmp.lua

Auto completion with Tailwind CSS color preview

HLJS LUA
return {
  "hrsh7th/nvim-cmp",
  event = "InsertEnter",
  dependencies = {
    "hrsh7th/cmp-buffer",
    "hrsh7th/cmp-path",
    "hrsh7th/cmp-cmdline",
    {
      "L3MON4D3/LuaSnip",
      version = "v2.*",
      build = "make install_jsregexp",
    },
    "saadparwaiz1/cmp_luasnip",
    "rafamadriz/friendly-snippets",
    "onsails/lspkind.nvim",
    "roobert/tailwindcss-colorizer-cmp.nvim",
  },
  opts = function()
    vim.api.nvim_set_hl(0, "CmpGhostText", { link = "Comment", default = true })
    local cmp = require("cmp")
    local defaults = require("cmp.config.default")()
    local auto_select = true
    return {
      auto_brackets = {}, -- configure any filetype to auto add brackets
      completion = {
        completeopt = "menu,menuone,noinsert" .. (auto_select and "" or ",noselect"),
      },
      preselect = auto_select and cmp.PreselectMode.Item or cmp.PreselectMode.None,
      mapping = cmp.mapping.preset.insert({
        ["<C-b>"] = cmp.mapping.scroll_docs(-4),
        ["<C-f>"] = cmp.mapping.scroll_docs(4),
        ["<C-n>"] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Insert }),
        ["<C-p>"] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Insert }),
        ["<C-Space>"] = cmp.mapping.complete(),
        ["<CR>"] = LazyVim.cmp.confirm({ select = auto_select }),
        ["<C-y>"] = LazyVim.cmp.confirm({ select = true }),
        ["<S-CR>"] = LazyVim.cmp.confirm({ behavior = cmp.ConfirmBehavior.Replace }),
        ["<C-CR>"] = function(fallback)
          cmp.abort()
          fallback()
        end,
      }),
      sources = cmp.config.sources({
        { name = "nvim_lsp" },
        { name = "luasnip" },
        { name = "path" },
      }, {
        { name = "buffer" },
      }),
      formatting = {
        format = function(entry, item)
          local color_item = require("tailwindcss-colorizer-cmp").formatter(entry, item)
          item = require("lspkind").cmp_format({
            mode = "symbol_text",
            maxwidth = 50,
            ellipsis_char = "...",
            show_labelDetails = true,
            symbol_map = {
              Copilot = "",
            },
          })(entry, item)
          if color_item.abbr_hl_group then
            item.kind_hl_group = color_item.abbr_hl_group
            item.kind = color_item.abbr
          end
          return item
        end,
      },
      experimental = {
        ghost_text = {
          hl_group = "CmpGhostText",
        },
      },
      sorting = defaults.sorting,
    }
  end,
}

2. Code Formatting & Quality

formatting.nvim automatically formats your React/TypeScript code on save using Prettier and ESLint. This ensures consistent code style across your team and catches common React patterns like unused imports or missing dependencies in useEffect hooks.

formatting.lua

Auto-format on save with conform.nvim

HLJS LUA
return {
  "stevearc/conform.nvim",
  event = { "BufWritePre" },
  cmd = { "ConformInfo" },
  keys = {
    {
      "<leader>f",
      function()
        require("conform").format({ async = true })
      end,
      mode = "",
      desc = "Format buffer",
    },
  },
  opts = {
    formatters_by_ft = {
      javascript = { "prettier" },
      typescript = { "prettier" },
      javascriptreact = { "prettier" },
      typescriptreact = { "prettier" },
      vue = { "prettier" },
      css = { "prettier" },
      scss = { "prettier" },
      less = { "prettier" },
      html = { "prettier" },
      json = { "prettier" },
      jsonc = { "prettier" },
      yaml = { "prettier" },
      markdown = { "prettier" },
      ["markdown.mdx"] = { "prettier" },
      graphql = { "prettier" },
      handlebars = { "prettier" },
      lua = { "stylua" },
      python = { "isort", "black" },
    },
    default_format_opts = {
      lsp_format = "fallback",
    },
    format_on_save = {
      timeout_ms = 500,
      lsp_format = "fallback",
    },
    formatters = {
      shfmt = {
        prepend_args = { "-i", "2" },
      },
    },
  },
  init = function()
    vim.o.formatexpr = "v:lua.require'conform'.formatexpr()"
  end,
}

3. React/JSX Enhancements

autopair.nvim intelligently pairs brackets, quotes, and JSX tags, understanding React's syntax nuances. autotag.nvim automatically closes HTML and JSX tags as you type, essential for React development where you're constantly writing component markup.

autopair.lua

Smart bracket pairing for JSX/TSX

HLJS LUA
return {
  "windwp/nvim-autopairs",
  event = "InsertEnter",
  dependencies = { "hrsh7th/nvim-cmp" },
  config = function()
    require("nvim-autopairs").setup({
      check_ts = true,
      ts_config = {
        lua = { "string", "source" },
        javascript = { "string", "template_string" },
        java = false,
      },
      disable_filetype = { "TelescopePrompt", "spectre_panel" },
      fast_wrap = {
        map = "<M-e>",
        chars = { "{", "[", "(", '"', "'" },
        pattern = string.gsub([[ [%'%"%)%>%]%)%}%,] ]], "%s+", ""),
        offset = 0,
        end_key = "$",
        keys = "qwertyuiopzxcvbnmasdfghjkl",
        check_comma = true,
        highlight = "PmenuSel",
        highlight_grey = "LineNr",
      },
    })

    local cmp_autopairs = require("nvim-autopairs.completion.cmp")
    local cmp = require("cmp")
    cmp.event:on("confirm_done", cmp_autopairs.on_confirm_done())
  end,
}
autotag.lua

Auto-close HTML/JSX tags

HLJS LUA
return {
  "windwp/nvim-ts-autotag",
  event = "LazyFile",
  opts = {},
}

UI & Appearance

4. Interface Plugins

alpha.nvim creates a clean startup dashboard showing recent files and project shortcuts, perfect for quickly jumping into React projects. lualine.nvim provides an informative statusline displaying Git branch, LSP status, and current file information. noice.nvim enhances the command interface with modern popups and notifications, while telescope.nvim offers powerful fuzzy finding for files, symbols, and even React components across your project.

telescope.lua

Fuzzy finder with custom keymaps

HLJS LUA
return {
  "nvim-telescope/telescope.nvim",
  cmd = "Telescope",
  version = false,
  dependencies = {
    "nvim-lua/plenary.nvim",
    {
      "nvim-telescope/telescope-fzf-native.nvim",
      build = "make",
      enabled = vim.fn.executable("make") == 1,
      config = function()
        LazyVim.on_load("telescope.nvim", function()
          require("telescope").load_extension("fzf")
        end)
      end,
    },
  },
  keys = {
    {
      "<leader>,",
      "<cmd>Telescope buffers sort_mru=true sort_lastused=true<cr>",
      desc = "Switch Buffer",
    },
    { "<leader>/", LazyVim.pick("live_grep"), desc = "Grep (Root Dir)" },
    { "<leader>:", "<cmd>Telescope command_history<cr>", desc = "Command History" },
    { "<leader><space>", LazyVim.pick("files"), desc = "Find Files (Root Dir)" },
    -- find
    { "<leader>fb", "<cmd>Telescope buffers sort_mru=true sort_lastused=true<cr>", desc = "Buffers" },
    { "<leader>fc", LazyVim.pick.config_files(), desc = "Find Config File" },
    { "<leader>ff", LazyVim.pick("files"), desc = "Find Files (Root Dir)" },
    { "<leader>fF", LazyVim.pick("files", { root = false }), desc = "Find Files (cwd)" },
    { "<leader>fg", "<cmd>Telescope git_files<cr>", desc = "Find Files (git-files)" },
    { "<leader>fr", "<cmd>Telescope oldfiles<cr>", desc = "Recent" },
    { "<leader>fR", LazyVim.pick("oldfiles", { cwd = vim.uv.cwd() }), desc = "Recent (cwd)" },
    -- git
    { "<leader>gc", "<cmd>Telescope git_commits<CR>", desc = "Commits" },
    { "<leader>gs", "<cmd>Telescope git_status<CR>", desc = "Status" },
    -- search
    { '<leader>s"', "<cmd>Telescope registers<cr>", desc = "Registers" },
    { "<leader>sa", "<cmd>Telescope autocommands<cr>", desc = "Auto Commands" },
    { "<leader>sb", "<cmd>Telescope current_buffer_fuzzy_find<cr>", desc = "Buffer" },
    { "<leader>sc", "<cmd>Telescope command_history<cr>", desc = "Command History" },
    { "<leader>sC", "<cmd>Telescope commands<cr>", desc = "Commands" },
    { "<leader>sd", "<cmd>Telescope diagnostics bufnr=0<cr>", desc = "Document Diagnostics" },
    { "<leader>sD", "<cmd>Telescope diagnostics<cr>", desc = "Workspace Diagnostics" },
    { "<leader>sg", LazyVim.pick("live_grep"), desc = "Grep (Root Dir)" },
    { "<leader>sG", LazyVim.pick("live_grep", { root = false }), desc = "Grep (cwd)" },
    { "<leader>sh", "<cmd>Telescope help_tags<cr>", desc = "Help Pages" },
    { "<leader>sH", "<cmd>Telescope highlights<cr>", desc = "Search Highlight Groups" },
    { "<leader>sj", "<cmd>Telescope jumplist<cr>", desc = "Jumplist" },
    { "<leader>sk", "<cmd>Telescope keymaps<cr>", desc = "Key Maps" },
    { "<leader>sl", "<cmd>Telescope loclist<cr>", desc = "Location List" },
    { "<leader>sM", "<cmd>Telescope man_pages<cr>", desc = "Man Pages" },
    { "<leader>sm", "<cmd>Telescope marks<cr>", desc = "Jump to Mark" },
    { "<leader>so", "<cmd>Telescope vim_options<cr>", desc = "Options" },
    { "<leader>sR", "<cmd>Telescope resume<cr>", desc = "Resume" },
    { "<leader>sq", "<cmd>Telescope quickfix<cr>", desc = "Quickfix List" },
    { "<leader>sw", LazyVim.pick("grep_string", { word_match = "-w" }), desc = "Word (Root Dir)" },
    { "<leader>sW", LazyVim.pick("grep_string", { root = false, word_match = "-w" }), desc = "Word (cwd)" },
    { "<leader>sw", LazyVim.pick("grep_string"), mode = "v", desc = "Selection (Root Dir)" },
    { "<leader>sW", LazyVim.pick("grep_string", { root = false }), mode = "v", desc = "Selection (cwd)" },
    { "<leader>uC", LazyVim.pick("colorscheme", { enable_preview = true }), desc = "Colorscheme with Preview" },
    {
      "<leader>ss",
      function()
        require("telescope.builtin").lsp_document_symbols({
          symbols = LazyVim.config.get_kind_filter(),
        })
      end,
      desc = "Goto Symbol",
    },
    {
      "<leader>sS",
      function()
        require("telescope.builtin").lsp_dynamic_workspace_symbols({
          symbols = LazyVim.config.get_kind_filter(),
        })
      end,
      desc = "Goto Symbol (Workspace)",
    },
  },
  opts = function()
    local actions = require("telescope.actions")

    local open_with_trouble = function(...)
      return require("trouble.sources.telescope").open(...)
    end
    local find_files_no_ignore = function()
      local action_state = require("telescope.actions.state")
      local line = action_state.get_current_line()
      LazyVim.pick("find_files", { no_ignore = true, default_text = line })()
    end
    local find_files_with_hidden = function()
      local action_state = require("telescope.actions.state")
      local line = action_state.get_current_line()
      LazyVim.pick("find_files", { hidden = true, default_text = line })()
    end

    return {
      defaults = {
        prompt_prefix = " ",
        selection_caret = " ",
        get_selection_window = function()
          local wins = vim.api.nvim_list_wins()
          table.insert(wins, 1, vim.api.nvim_get_current_win())
          for _, win in ipairs(wins) do
            local buf = vim.api.nvim_win_get_buf(win)
            if vim.bo[buf].buftype == "" then
              return win
            end
          end
          return 0
        end,
        mappings = {
          i = {
            ["<c-t>"] = open_with_trouble,
            ["<a-t>"] = open_with_trouble,
            ["<a-i>"] = find_files_no_ignore,
            ["<a-h>"] = find_files_with_hidden,
            ["<C-Down>"] = actions.cycle_history_next,
            ["<C-Up>"] = actions.cycle_history_prev,
            ["<C-f>"] = actions.preview_scrolling_down,
            ["<C-b>"] = actions.preview_scrolling_up,
          },
          n = {
            ["q"] = actions.close,
          },
        },
      },
    }
  end,
}

5. Visual Enhancements

colorscheme.nvim provides consistent theming that's easy on the eyes during long coding sessions. colorizer.nvim is particularly valuable for React development, instantly previewing hex colors and Tailwind CSS classes directly in your code, eliminating the need to constantly switch to browser dev tools to check colors.

colorizer.lua

Hex color & Tailwind CSS color preview

HLJS LUA
return {
  "NvChad/nvim-colorizer.lua",
  event = "LazyFile",
  opts = {
    filetypes = {
      "typescript",
      "typescriptreact",
      "javascript",
      "javascriptreact",
      "css",
      "html",
      "astro",
      "lua",
      "vue",
      "less",
      "scss",
      "sass",
      "stylus",
    },
    user_default_options = {
      RGB = true, -- #RGB hex codes
      RRGGBB = true, -- #RRGGBB hex codes
      names = false, -- "Name" codes like Blue or Gray
      RRGGBBAA = true, -- #RRGGBBAA hex codes
      AARRGGBB = false, -- 0xAARRGGBB hex codes
      rgb_fn = true, -- CSS rgb() and rgba() functions
      hsl_fn = true, -- CSS hsl() and hsla() functions
      css = true, -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB
      css_fn = true, -- Enable all CSS *functions*: rgb_fn, hsl_fn
      mode = "background", -- Set the display mode
      tailwind = true,
      sass = { enable = true, parsers = { "css" } },
      virtualtext = "■",
      always_update = false,
    },
    buftypes = {},
  },
}

Productivity Tools

6. File Management & Navigation

nvim-tree.nvim provides a familiar file explorer for navigating React project structures, with built-in Git integration showing file status. toggleterm.nvim offers seamless terminal integration, essential for running npm scripts, starting development servers, and managing multiple terminal instances for different tasks like npm run dev, npm run build, and npm test.

toggleterm.lua

Integrated terminal for npm/yarn commands

HLJS LUA
return {
  "akinsho/toggleterm.nvim",
  version = "*",
  opts = {
    size = 20,
    open_mapping = [[<c-\>]],
    hide_numbers = true,
    shade_terminals = true,
    start_in_insert = true,
    insert_mappings = true,
    terminal_mappings = true,
    persist_size = true,
    persist_mode = true,
    direction = "float",
    close_on_exit = true,
    shell = vim.o.shell,
    auto_scroll = true,
    float_opts = {
      border = "curved",
      winblend = 0,
      highlights = {
        border = "Normal",
        background = "Normal",
      },
    },
  },
  config = function(_, opts)
    require("toggleterm").setup(opts)
    
    -- Custom terminal commands for React development
    local Terminal = require("toggleterm.terminal").Terminal
    
    -- Lazygit terminal
    local lazygit = Terminal:new({
      cmd = "lazygit",
      hidden = true,
      direction = "float",
      float_opts = {
        border = "none",
        width = 100000,
        height = 100000,
      },
      on_open = function(_)
        vim.cmd("startinsert!")
      end,
      on_close = function(_)
        vim.cmd("startinsert!")
      end,
      count = 99,
    })
    
    -- Keymaps
    vim.keymap.set("n", "<leader>lg", function() lazygit:toggle() end, { desc = "LazyGit" })
    vim.keymap.set("n", "<leader>td", function()
      vim.cmd("TermExec cmd='npm run dev'")
    end, { desc = "npm run dev" })
    vim.keymap.set("n", "<leader>tb", function()
      vim.cmd("TermExec cmd='npm run build'")
    end, { desc = "npm run build" })
    vim.keymap.set("n", "<leader>tt", function()
      vim.cmd("TermExec cmd='npm test'")
    end, { desc = "npm test" })
  end,
}

7. Code Manipulation

comment.nvim intelligently handles commenting in JSX/TSX files, understanding when to use // for JavaScript and {/* */} for JSX elements. surround.nvim is incredibly powerful for React development, allowing you to quickly wrap JSX elements with <div>, <Fragment>, or any other component. snippets.nvim provides custom React/Next.js code snippets for common patterns like functional components, hooks, and API routes.

comment.lua

Smart commenting for JSX/TSX

HLJS LUA
return {
  "numToStr/Comment.nvim",
  event = "LazyFile",
  dependencies = {
    "JoosepAlviste/nvim-ts-context-commentstring",
  },
  config = function()
    require("Comment").setup({
      pre_hook = require("ts_context_commentstring.integrations.comment_nvim").create_pre_hook(),
    })
  end,
}
surround.lua

Wrap selections with JSX elements

HLJS LUA
return {
  "kylechui/nvim-surround",
  version = "*",
  event = "VeryLazy",
  config = function()
    require("nvim-surround").setup({
      -- Configuration for React/JSX
      surrounds = {
        -- Add custom JSX surrounds
        ["d"] = {
          add = { "<div>", "</div>" },
          find = "<div.->(.-)</div>",
          delete = "^(<div.->)().-()</(div)>$",
        },
        ["f"] = {
          add = { "<>", "</>" },
          find = "<>.-</>",
          delete = "^(<>)().-(</>.-)$",
        },
      },
    })
  end,
}

Specialized Plugins

8. Personal Workflow Enhancements

markdown.nvim renders markdown files beautifully, essential for maintaining project documentation and README files in React projects. leetcode.nvim integrates coding practice directly into your editor, allowing you to sharpen your algorithmic skills without leaving your development environment. zen.nvim creates a distraction-free coding mode by hiding sidebars and unnecessary UI elements, perfect for deep focus sessions when working on complex React components or debugging intricate state management.

zen-mode.lua

Distraction-free coding mode

HLJS LUA
return {
  "folke/zen-mode.nvim",
  cmd = "ZenMode",
  opts = {
    window = {
      backdrop = 0.95,
      width = 120,
      height = 1,
      options = {
        signcolumn = "no",
        number = false,
        relativenumber = false,
        cursorline = false,
        cursorcolumn = false,
        foldcolumn = "0",
        list = false,
      },
    },
    plugins = {
      options = {
        enabled = true,
        ruler = false,
        showcmd = false,
        laststatus = 0,
      },
      twilight = { enabled = true },
      gitsigns = { enabled = false },
      tmux = { enabled = false },
      todo = { enabled = false },
    },
  },
  keys = { 
    { "<leader>z", "<cmd>ZenMode<cr>", desc = "Zen Mode" } 
  },
}
markdown.lua

Beautiful markdown rendering

HLJS LUA
return {
  "MeanderingProgrammer/render-markdown.nvim",
  opts = {
    file_types = { "markdown", "norg", "rmd", "org" },
    code = {
      sign = false,
      width = "block",
      right_pad = 1,
    },
    heading = {
      sign = false,
      icons = { "󰲡 ", "󰲣 ", "󰲥 ", "󰲧 ", "󰲩 ", "󰲫 " },
    },
  },
  ft = { "markdown", "norg", "rmd", "org" },
  config = function(_, opts)
    require("render-markdown").setup(opts)
    LazyVim.toggle.map("<leader>um", {
      name = "Render Markdown",
      get = function()
        return require("render-markdown.state").enabled
      end,
      set = function(enabled)
        local m = require("render-markdown")
        if enabled then
          m.enable()
        else
          m.disable()
        end
      end,
    })
  end,
}
leetcode.lua

Coding practice integration

HLJS LUA
return {
  "kawre/leetcode.nvim",
  build = ":TSUpdate html",
  dependencies = {
    "nvim-telescope/telescope.nvim",
    "nvim-lua/plenary.nvim",
    "MunifTanjim/nui.nvim",
    "nvim-treesitter/nvim-treesitter",
    "rcarriga/nvim-notify",
    "nvim-tree/nvim-web-devicons",
  },
  opts = {
    arg = "leetcode",
    lang = "typescript",
    cn = {
      enabled = false,
    },
    storage = {
      home = vim.fn.stdpath("data") .. "/leetcode",
      cache = vim.fn.stdpath("cache") .. "/leetcode",
    },
    logging = true,
    injector = {
      ["typescript"] = {
        before = "export {};",
      },
      ["javascript"] = {
        before = "// @ts-check",
      },
    },
    cache = {
      update_interval = 60 * 60 * 24 * 7,
    },
    console = {
      open_on_runcode = true,
      dir = "row",
      size = {
        width = "90%",
        height = "75%",
      },
      result = {
        size = "60%",
      },
      testcase = {
        virt_text = true,
        size = "40%",
      },
    },
    description = {
      position = "left",
      width = "40%",
      show_stats = true,
    },
    hooks = {
      ["enter"] = {},
      ["question_enter"] = {},
      ["leave"] = {},
    },
    keys = {
      toggle = { "q" },
      confirm = { "<CR>" },
      reset_testcases = "r",
      use_testcase = "U",
      focus_testcases = "H",
      focus_result = "L",
    },
    theme = {},
    image_support = false,
  },
  config = function(_, opts)
    require("leetcode").setup(opts)
  end,
}

Resources

Essential Neovim Resources

Plugin Discovery & Configuration

  • Browse community configurations on Dotfyle for inspiration
  • Check LazyVim extras for pre-configured plugin setups
  • Join the Neovim community on Reddit and Discord for tips and troubleshooting

Thanks for reading! Hope you enjoyed this little adventure through words and ideas.

Published September 21, 20255170 words26 min read