Nvim Statusline
A customizable statusline component for Neovim, inspired by the nvim-lualine plugin.
Component Source'use client'import { Button } from '@/components/ui/button'import { GitBranchIcon } from 'lucide-react'import { NvimStatusline, NvimStatuslineProvider, NvimStatuslineSectionA, NvimStatuslineSectionB, NvimStatuslineSectionC, NvimStatuslineSectionX, NvimStatuslineSectionY, NvimStatuslineSectionZ, useNvimStatusline,} from '@/components/ui/nvim-statusline'export default function NvimStatuslineDemo() { return ( <NvimStatuslineProvider> <div className='flex min-h-40 w-full flex-col'> <ChangeModeButtons /> <NvimStatuslineContent /> </div> </NvimStatuslineProvider> )}function NvimStatuslineContent() { const { mode } = useNvimStatusline() return ( <NvimStatusline> <NvimStatuslineSectionA className='font-bold'> {mode.toUpperCase()} </NvimStatuslineSectionA> <NvimStatuslineSectionB> <GitBranchIcon /> main </NvimStatuslineSectionB> <NvimStatuslineSectionC>~/app/page.tsx</NvimStatuslineSectionC> <NvimStatuslineSectionX>+15</NvimStatuslineSectionX> <NvimStatuslineSectionY>Top 1:1</NvimStatuslineSectionY> <NvimStatuslineSectionZ className='font-bold'> {new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', })} </NvimStatuslineSectionZ> </NvimStatusline> )}function ChangeModeButtons() { const { modes, setMode } = useNvimStatusline() return ( <div className='container flex flex-1 flex-wrap items-center gap-4 py-6'> {modes.map((mode) => ( <Button key={mode} variant='outline' style={{ color: `var(--${mode})`, }} size='sm' onClick={() => { setMode(mode) }} > {mode.toUpperCase()} </Button> ))} </div> )}
Installation
CLI
npx shadcn add https://ui.tiesen.id.vn/r/nvim-statusline.json
npx shadcn add https://ui.tiesen.id.vn/r/nvim-statusline.json
pnpm dlx shadcn add https://ui.tiesen.id.vn/r/nvim-statusline.json
bunx --bun shadcn add https://ui.tiesen.id.vn/r/nvim-statusline.json
Manual
Install the following dependencies:
npm install @radix-ui/react-slot
Add the following CSS variables to your globals.css file:
@theme inline {
...
--color-normal: var(--normal);
--color-visual: var(--visual);
--color-replace: var(--replace);
--color-insert: var(--insert);
--color-terminal: var(--terminal);
--color-command: var(--command);
}
:root {
...
--normal: oklch(0.533 0.188299 256.8803);
--visual: oklch(0.5945 0.1522 48.09);
--replace: oklch(0.5352 0.1882 2.43);
--insert: oklch(0.6273 0.17 149.2);
--terminal: oklch(0.4706 0.2205 304.22);
--command: oklch(0.6424 0.18 45.27);
}
.dark {
...
--normal: oklch(0.7178 0.1521 250.77);
--visual: oklch(0.8535 0.0907 84.06);
--replace: oklch(0.6931 0.1891 3.82);
--insert: oklch(0.6273 0.17 149.2);
--terminal: oklch(0.6986 0.1786 309.44);
--command: oklch(0.8124 0.1238 55.54);
}
Copy and paste the following code into your project.
'use client'import * as React from 'react'import { Slot } from '@radix-ui/react-slot'import { cn } from '@/lib/utils'const MODES = [ 'normal', 'visual', 'replace', 'insert', 'terminal', 'command',] as consttype Mode = (typeof MODES)[number]const BG_COLORS = 'group-data-[mode=normal]/statusline:bg-normal group-data-[mode=visual]/statusline:bg-visual group-data-[mode=insert]/statusline:bg-insert group-data-[mode=replace]/statusline:bg-replace group-data-[mode=command]/statusline:bg-command group-data-[mode=terminal]/statusline:bg-terminal'const TEXT_COLORS = 'group-data-[mode=normal]/statusline:text-normal group-data-[mode=visual]/statusline:text-visual group-data-[mode=insert]/statusline:text-insert group-data-[mode=replace]/statusline:text-replace group-data-[mode=command]/statusline:text-command group-data-[mode=terminal]/statusline:text-terminal'const FILL_COLORS = 'group-data-[mode=normal]/statusline:fill-normal group-data-[mode=visual]/statusline:fill-visual group-data-[mode=insert]/statusline:fill-insert group-data-[mode=replace]/statusline:fill-replace group-data-[mode=command]/statusline:fill-command group-data-[mode=terminal]/statusline:fill-terminal'interface NvimStatuslineContextValue { mode: Mode modes: typeof MODES setMode: React.Dispatch<React.SetStateAction<Mode>>}const NvimStatuslineContext = React.createContext<NvimStatuslineContextValue | null>(null)function useNvimStatusline() { const context = React.use(NvimStatuslineContext) if (context === null) throw new Error( 'useNvimStatusline must be used within a NvimStatuslineProvider', ) return context}function NvimStatuslineProvider({ children,}: Readonly<{ children: React.ReactNode}>) { const [mode, setMode] = React.useState<Mode>('normal') const value = React.useMemo(() => ({ mode, modes: MODES, setMode }), [mode]) return <NvimStatuslineContext value={value}>{children}</NvimStatuslineContext>}function NvimStatusline({ className, asChild = false, ...props}: React.ComponentProps<'footer'> & { asChild?: boolean }) { const { mode } = useNvimStatusline() const Comp = asChild ? Slot : 'footer' return ( <Comp data-slot='nvim-statusline' data-mode={mode} className={cn( 'group/statusline sticky bottom-0 left-0 z-50 flex h-6 w-full items-center justify-between gap-0 bg-secondary px-4 font-mono text-secondary-foreground md:bottom-4', "[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} /> )}function NvimStatuslineSectionA({ className, children, ...props}: React.ComponentProps<'div'>) { return ( <div data-slot='nvim-statusline-section-a' className={cn('inline-flex h-full shrink-0 items-center', className)} {...props} > <div className={cn( 'inline-flex h-full items-center gap-2 px-2 text-background', BG_COLORS, )} > {children} </div> <NvimStatuslineSectionSeparator data-slot='nvim-statusline-section-a-separator' className={cn('size-6 rotate-90 bg-background', FILL_COLORS)} /> </div> )}function NvimStatuslineSectionB({ className, children, ...props}: React.ComponentProps<'div'>) { return ( <div data-slot='nvim-statusline-section-b' className={cn( 'inline-flex h-full items-center overflow-hidden', className, )} {...props} > <div className={cn( 'inline-flex h-full items-center gap-2 bg-background pr-2 whitespace-nowrap', TEXT_COLORS, )} > {children} </div> <NvimStatuslineSectionSeparator data-slot='nvim-statusline-section-b-separator' className='size-6 rotate-90 bg-secondary fill-background' /> </div> )}function NvimStatuslineSectionC({ className, ...props}: React.ComponentProps<'div'>) { return ( <div data-slot='nvim-statusline-section-c' className={cn( 'inline-flex h-full max-w-full flex-1 items-center gap-2 truncate overflow-hidden bg-secondary pr-2 text-ellipsis whitespace-nowrap text-secondary-foreground', className, )} {...props} /> )}function NvimStatuslineSectionX({ className, ...props}: React.ComponentProps<'div'>) { return ( <div data-slot='nvim-statusline-section-x' className={cn( 'inline-flex h-full items-center gap-2 truncate overflow-hidden bg-secondary pl-2 text-ellipsis whitespace-nowrap text-secondary-foreground', className, )} {...props} /> )}function NvimStatuslineSectionY({ className, children, ...props}: React.ComponentProps<'div'>) { return ( <div data-slot='nvim-statusline-section-y' className={cn( 'inline-flex h-full items-center overflow-hidden', className, )} {...props} > <NvimStatuslineSectionSeparator data-slot='nvim-statusline-section-y-separator' className='size-6 rotate-270 bg-secondary fill-background' /> <div className={cn( 'inline-flex h-full items-center gap-2 bg-background pl-2 whitespace-nowrap', TEXT_COLORS, )} > {children} </div> </div> )}function NvimStatuslineSectionZ({ className, children, ...props}: React.ComponentProps<'div'>) { return ( <div data-slot='nvim-statusline-section-z' className={cn('inline-flex h-full shrink-0 items-center', className)} {...props} > <NvimStatuslineSectionSeparator data-slot='nvim-statusline-section-z-separator' className={cn('size-6 rotate-270 bg-background', FILL_COLORS)} /> <div className={cn( 'inline-flex h-full items-center gap-2 px-2 whitespace-nowrap text-background', BG_COLORS, )} > {children} </div> </div> )}const NvimStatuslineSectionSeparator = (props: React.ComponentProps<'svg'>) => { return ( <svg {...props} role='img' viewBox='0 0 24 4' xmlns='http://www.w3.org/2000/svg' > <title>Nvim Statusline Section Separator</title> <path d='m12 3.4 12 10.784H0Z' /> </svg> )}export { useNvimStatusline, NvimStatusline, NvimStatuslineProvider, NvimStatuslineSectionA, NvimStatuslineSectionB, NvimStatuslineSectionC, NvimStatuslineSectionX, NvimStatuslineSectionY, NvimStatuslineSectionZ, NvimStatuslineSectionSeparator,}
Structure
The NvimStatusline
component follows a modular architecture with these key parts:
NvimStatuslineProvider
- Context provider that manages statusline modes (normal, insert, visual, etc.)NvimStatusline
- Main container component that renders the statusline layoutNvimStatuslineSection{A-Z}
- Individual section components for organizing content within the statusline
The statusline is divided into six sections arranged as follows:
┌───┬───┬───────────────┬───┬───┬───┐
│ A │ B │ C │ X │ Y │ Z │
└───┴───┴───────────────┴───┴───┴───┘
Usage
Initialize the statusline provider to enable mode management across your application
import { NvimStatuslineProvider } from '@/components/ui/nvim-statusline'
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang='en'>
<body>
<NvimStatuslineProvider>{children}</NvimStatuslineProvider>
</body>
</html>
)
}
Build your statusline layout using the provided section components
import {
NvimStatusline,
NvimStatuslineSectionA,
NvimStatuslineSectionB,
NvimStatuslineSectionC,
NvimStatuslineSectionX,
NvimStatuslineSectionY,
NvimStatuslineSectionZ,
} from '@/components/ui/nvim-statusline'
export function Statusline() {
return (
<NvimStatusline>
<NvimStatuslineSectionA />
<NvimStatuslineSectionB />
<NvimStatuslineSectionC />
<NvimStatuslineSectionX />
<NvimStatuslineSectionY />
<NvimStatuslineSectionZ />
</NvimStatusline>
)
}
Integrate the statusline component into your application layout
import { Statusline } from '@/components/statusline.tsx'
import { NvimStatuslineProvider } from '@/components/ui/nvim-statusline'
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang='en'>
<body>
<NvimStatuslineProvider>
{children}
<Statusline />
</NvimStatuslineProvider>
</body>
</html>
)
}
Control statusline modes programmatically using the provided hook
const { setMode } = useNvimStatusline()
setMode('insert')
:::info
If you don't use sections B or Y, add this class to NvimStatusline
to hide the separators's background for sections A and Z:
<NvimStatusline className='[&_[data-slot=nvim-statusline-section-a-separator]]:bg-transparent [&_[data-slot=nvim-statusline-section-z-separator]]:bg-transparent' />
:::
References
- nvim-lualine/lualine.nvim - A blazing fast and easy to configure Neovim statusline plugin