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
# 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
# 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
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
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
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
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
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
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
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
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
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
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
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
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
- LazyVim Documentation: Official documentation for the LazyVim distribution
- Dotfyle: Discover and share Neovim configurations and plugins
- Neovim Documentation: Official Neovim documentation and guides
- Awesome Neovim: Curated list of Neovim plugins and 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