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 const
type 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')
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