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

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