Now that the plugin manager is installed, it is time to make Neovim look stunning: choose a modern colorscheme like Tokyo Night and Catppuccin, build an informative statusline with lualine.nvim, then complete it with bufferline, indent guides, and devicons for an editor experience that is comfortable to look at for hours.

After covering the modern plugin manager lazy.nvim in episode 9 — from the bootstrap script in init.lua, the modular lua/plugins/*.lua structure, to using the :Lazy UI to install, update, and clean plugins — you now have a foundation that lets you extend Neovim with an almost unlimited plugin ecosystem. In this episode, we will use that foundation to transform Neovim's appearance from a "plain editor" into a comfortable-to-look-at IDE.
Why does this topic matter in the real working world? Because as an engineer, you can spend 8+ hours a day staring at the editor screen. Wrong colors, poor contrast, and an uninformative statusline are not just about aesthetics — they concern eye health, focus endurance, and working speed. In this episode we will build four layers of visual customization at once: the colorscheme as the color foundation, lualine.nvim as the statusline, bufferline.nvim as a VS Code-style tab bar, plus indent-blankline and nvim-web-devicons as detail finishers. Let's begin.
Try to think of your Neovim as an airplane cockpit. Every pilot has different instrument layout preferences, but the goal is the same: read important information in a split second without shifting their gaze. The statusline is our main instrument panel — it tells us which file is open, which git branch we are on, whether the file has unsaved changes, which line the cursor is on, and which editing mode is active. Without it, we often get "lost" inside large projects.
But there is a more fundamental principle: contrast and color consistency affect the brain's parsing speed. When syntax is colored consistently, your eyes automatically recognize patterns — strings are always green, functions are always blue, keywords are always purple — without reading the text character by character. This is why choosing a good colorscheme (high contrast, not flashy, consistent) is just as important as choosing a readable font.
Note
Before getting into plugins, make sure the two visual prerequisites from episode 0 are in place: your terminal supports True Color (termguicolors) and you are already using a Nerd Font. We will use both as the stepping stones for all of Phase 3.
Default Neovim uses a limited and sometimes boring color scheme. Modern colorschemes bring three things: a designed color palette (not random), Treesitter support (accurate highlighting per token type, not just per keyword), and plugin integration (statusline, LSP, git signs all blend in). This is why themes like Tokyo Night or Catppuccin are so popular — not just because they are pretty, but because of their consistency across the entire UI.
Here are the four themes the community uses most:
| Theme | Style | Impression | Highlights |
|---|---|---|---|
| Tokyo Night | Dark with blue-purple accents | Modern, high contrast, focused | tokyonight-night, tokyonight-storm |
| Catppuccin | Soft pastel, 4 flavours | Soft, easy on the eyes | latte, frappe, macchiato, mocha |
| Gruvbox | Retro, warm (orange/cream) | Comfortable for long sessions | gruvbox-material vs classic gruvbox |
| Kanagawa | Inspired by ukiyo-e paintings | Calm, elegant, deep dark | wave, dragon, lotus |
None is "most correct" — this choice is subjective and very personal. Practical advice: pick one theme with the darkest flavour as your default, because most developers code in low-light environments. You can change it anytime, and in episode 11 we will install a colorscheme picker so switching themes does not require editing files.
Since we are already using lazy.nvim, installing a theme is just adding one plugin spec. The two most popular examples:
return {
{
"folke/tokyonight.nvim",
lazy = false,
priority = 1000,
opts = {
style = "night",
transparent = false,
terminal_colors = true,
},
config = function(_, opts)
require("tokyonight").setup(opts)
vim.cmd.colorscheme("tokyonight")
end,
},
}Two key attributes you must understand:
lazy = false — the theme must load before the UI renders, not on a specific event. If it is lazy-loaded, you will see a "white flash" (FOUC) when Neovim opens, because the colors only appear after the plugin finishes loading.priority = 1000 — lazy.nvim loads plugins with the highest priority first. The theme must win over other plugins so no plugin overrides the highlight colors.Important
The colors set inside opts only become active when the plugin loads. So do not forget to call vim.cmd.colorscheme("tokyonight") inside config after require(...).setup(opts). Many beginners put vim.cmd.colorscheme in init.lua before the plugin loads — the result: nothing happens, or worse, an E185: Cannot find color scheme error.
Tip
Not sure which theme to pick? In episode 11 we will install a colorscheme picker in telescope, which lets you switch themes live without editing files. So there is no reason to fear a wrong choice — install two or three themes at once, compare, then lock in your pick.
As an illustration of how to change theme options, the diff example below enables transparency on Tokyo Night — the -- line is the old line being removed, the ++ line is its replacement:
return {
{
"folke/tokyonight.nvim",
lazy = false,
priority = 1000,
opts = {
style = "night",
# [!code --:1]
transparent = false,
# [!code ++:1]
transparent = true,
terminal_colors = true,
},
config = function(_, opts)
require("tokyonight").setup(opts)
vim.cmd.colorscheme("tokyonight")
end,
},
}Remember the consequences: with transparent = true, Neovim's background follows the terminal color — beautiful if your terminal has a dark wallpaper, but hard to read if the wallpaper is light or the terminal does not provide a consistent dark background.
vim.api.nvim_set_hlEven a good colorscheme may not match your personal taste for certain highlight groups — for example you feel comments are too dim, or the CursorLine is not contrasting enough. This is where we learn one of the most useful APIs from episode 6: vim.api.nvim_set_hl (set highlight).
Every element on the Neovim screen — keywords, strings, line numbers, sign columns, cursor line — is represented by a named highlight group. Some groups are most often changed manually:
vim.api.nvim_set_hl(0, "Comment", { fg = "#565f89", italic = true })
vim.api.nvim_set_hl(0, "CursorLine", { bg = "#1c2433" })
vim.api.nvim_set_hl(0, "LineNr", { fg = "#3b4261" })
vim.api.nvim_set_hl(0, "CursorLineNr", { fg = "#7aa2f7", bold = true })Things to note:
0) means apply to the global namespace — applies to all buffers. You can also use nvim_create_namespace for per-buffer highlighting.#565f89). These values are taken from the theme's palette — the easiest way to find them is to run :hi Comment in Neovim and look at the current guifg value.nvim_set_hl calls must run after vim.cmd.colorscheme(...) is called.Note
A practical way to explore: run :Inspect (or :InspectTree if Treesitter is installed) with the cursor placed over any text — Neovim shows the active highlight group along with its values. The :Inspect + nvim_set_hl combination is the standard power-user recipe for "making a theme your own".
The built-in Vim/Neovim statusline (laststatus = 2) only shows the file name and cursor coordinates. lualine.nvim replaces it with a modern statusline that can be assembled like a rack of modules. Think of it like a car dashboard: we choose the speedometer module (mode), the fuel gauge (git branch), and the odometer (line/column) — as needed.
return {
{
"nvim-lualine/lualine.nvim",
event = "VeryLazy",
dependencies = { "nvim-tree/nvim-web-devicons" },
opts = {
options = {
theme = "auto",
section_separators = { left = "", right = "" },
component_separators = { left = "", right = "" },
globalstatus = true,
},
sections = {
lualine_a = { "mode" },
lualine_b = { "branch", "diff", "diagnostics" },
lualine_c = { { "filename", path = 1 } },
lualine_x = { "encoding", "fileformat", "filetype" },
lualine_y = { "progress" },
lualine_z = { "location" },
},
},
},
}Key points:
globalstatus = true — makes one statusline span all windows (instead of one statusline per split). This saves vertical space when you open many splits.sections — lualine_a through lualine_z are positions from left to right. The branch module uses built-in git, diff shows the number of changed lines, and diagnostics shows the error/warning count from LSP (we will build this in episode 15).theme = "auto" — lualine automatically reads the active colorscheme, so there is no need to hardcode colors that risk conflicting when you switch themes.Tip
The lualine_c section showing filename with path = 1 gives you file context in large projects: for example src/components/Button.tsx is shown instead of just Button.tsx. This is very helpful when opening files with the same name in different folders.
What makes lualine excel is its ability to accept custom components in the form of Lua functions. These components are re-run every time the state changes, so you can display information not provided by default. The most useful example is the LSP status indicator — it will feel fully relevant after we build LSP in episode 15, but let's set up the framework now:
sections = {
lualine_a = { "mode" },
lualine_b = { "branch", "diff", "diagnostics" },
lualine_c = { { "filename", path = 1 } },
lualine_x = {
{
function()
local clients = vim.lsp.get_clients({ bufnr = 0 })
if #clients == 0 then
return ""
end
local names = {}
for _, client in ipairs(clients) do
table.insert(names, client.name)
end
return table.concat(names, ", ")
end,
color = { fg = "#9ece6a", gui = "bold" },
},
"encoding",
"fileformat",
"filetype",
},
lualine_y = { "progress" },
lualine_z = { "location" },
},The function above calls vim.lsp.get_clients (the built-in Neovim API for checking which Language Servers are active in the current buffer) then shows the server names — for example gopls, tsserver, or lua_ls. When there is no LSP, an empty string is returned so the statusline stays clean. This is exactly the same pattern you will use for format-on-save indicators, linter status, and so on in the coming episodes.
If you are used to VS Code or JetBrains, open buffers usually look like tabs in the top bar. bufferline.nvim brings that experience to Neovim, complete with modification indicators, filetype icons, and more visual :bnext/:bprevious navigation.
return {
{
"akinsho/bufferline.nvim",
event = "VeryLazy",
version = "*",
opts = {
options = {
mode = "tabs",
diagnostics = "nvim_lsp",
always_show_tabline = true,
show_buffer_close_icons = false,
show_buffer_icons = true,
},
},
},
}Note the concept difference that often confuses people: tab pages (workspace layouts) are different from buffers (loaded files). Bufferline displays buffers, not tab pages. This is actually good because we rarely use tab pages (discussed in the episode on windows/tabs), while buffers are how we work every day — switching files without losing unsaved changes.
To navigate between bufferline tabs, you can use a combination of built-in commands and Neovim keymaps:
:BufferLinePick " pilih buffer dengan menekan angka/nomor
:BufferLineCycleNext " pindah ke buffer berikutnya
:BufferLineCyclePrev " pindah ke buffer sebelumnya
:bn " cara klasik: next buffer
:bp " cara klasik: prev bufferWhat you often see in other people's configs are <S-h> and <S-l> mappings for visually switching buffers — moving left/right like switching tabs in a browser. This is not built into bufferline, so just add it to lua/config/keymaps.lua:
vim.keymap.set("n", "<S-h>", ":BufferLineCyclePrev<CR>", { desc = "Buffer sebelumnya" })
vim.keymap.set("n", "<S-l>", ":BufferLineCycleNext<CR>", { desc = "Buffer berikutnya" })Two small plugins with a big impact on readability:
indent-blankline.nvim — draws vertical indentation guide lines. In languages like Python, YAML, or Lua where indentation matters, you can immediately see "how deep" this code block is without counting spaces manually.nvim-web-devicons — shows filetype-specific icons (.ts blue, .lua navy, .md gray, and so on) in the file explorer, bufferline, and pickers. This is what makes the editor feel "alive".return {
{
"lukas-reineke/indent-blankline.nvim",
event = "BufReadPre",
main = "ibl",
opts = {
indent = {
char = "│",
tab_char = "│",
},
scope = {
enabled = true,
show_start = true,
show_end = true,
},
},
},
{ "nvim-tree/nvim-web-devicons", lazy = true },
}Warning
nvim-web-devicons requires a Nerd Font. Without a patched font, icons appear as empty squares (tofu) that actually ruin the look. If you are not yet using a Nerd Font from episode 0, this is the time to install one (e.g. JetBrainsMono Nerd Font or FiraCode Nerd Font) and set it as the terminal's default font.
After both files above are created, run :Lazy then press I (Install), or simply restart Neovim. lazy.nvim will download all the plugins at once.
| Mistake | Symptom | Solution |
|---|---|---|
| Theme loaded lazily | White flash at startup | Set lazy = false and priority = 1000 |
vim.cmd.colorscheme executed too early | E185: Cannot find color scheme | Call it inside the plugin's config |
transparent = true without reason | Text hard to read over wallpaper | Set transparent = false or prepare a dark background |
nvim-web-devicons without a Nerd Font | Empty squares (tofu) on icons | Install & enable a Nerd Font in the terminal |
Forgot the devicons dependencies in lualine | Filetype icons do not appear in the statusline | Add nvim-web-devicons as a dependency |
globalstatus = false with many splits | Duplicate statuslines waste space | Set globalstatus = true |
In episode 10 we overhauled Neovim's appearance thoroughly: choosing and installing a modern colorscheme (Tokyo Night, Catppuccin, Gruvbox, or Kanagawa) with the correct lazy = false + priority = 1000 mechanism, building an informative statusline with lualine.nvim, adding a VS Code-style tab bar with bufferline.nvim, and indent guides plus file icons as readability finishers. The key to all of this is the correct load order and consistency between the theme, statusline, and other visual plugins.
Now your Neovim looks like a professional IDE. However, a beautiful editor is not necessarily fast — in episode 11 we will build a revolutionary file search and navigation engine with telescope.nvim, complete with C-based fzf-native acceleration. That is where your file-switching speed will truly be felt. Stay motivated!