Tiesen Logo
Components

Nvim Statusline

A customizable statusline component for Neovim, inspired by the nvim-lualine plugin.

GitHubComponent 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:

app/globals.css
@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 layout
  • NvimStatuslineSection{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

app/layout.tsx
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

app/layout.tsx
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