Ink Web
Guides

Loading States

Customize loading indicators for your terminal

Ink Web provides built-in loading states that display while your terminal initializes. Choose from three presets or provide your own custom component.

Quick Start

The simplest way to handle loading states with Next.js:

components/terminal.tsx
'use client'

import { createDynamicTerminal } from 'ink-web/next'

const Terminal = createDynamicTerminal(
  () => import('./my-terminal').then(m => m.MyTerminal),
  { rows: 15 }
)

export function TerminalWrapper() {
  return <Terminal />
}

This automatically:

  • Prevents SSR errors
  • Shows a loading spinner
  • Maintains consistent height
  • Handles smooth transitions

Loading Types

None (Default)

No loading indicator - just shows the terminal background until ready. This is the default behavior.

createDynamicTerminal(importFn, { 
  rows: 15, 
  loading: 'none' 
})

Spinner

A centered spinning loader icon.

Spinner
createDynamicTerminal(importFn, { 
  rows: 15, 
  loading: 'spinner' 
})

Skeleton

Multiple animated placeholder lines, similar to content loading states.

Skeleton
createDynamicTerminal(importFn, { 
  rows: 15, 
  loading: 'skeleton' 
})

Positioning

By default, loading indicators are centered. You can position them in any corner:

createDynamicTerminal(importFn, { 
  rows: 15, 
  loading: { type: 'skeleton', position: 'top-left' } 
})

Available positions:

  • 'center' (default)
  • 'top-left'
  • 'top-right'
  • 'bottom-left'
  • 'bottom-right'

Custom Loading Components

Pass any React element for full customization:

import { Loader2 } from 'lucide-react'

createDynamicTerminal(importFn, { 
  rows: 15, 
  loading: <Loader2 className="h-8 w-8 animate-spin text-blue-500" />
})

Or a more complex component:

function CustomLoader() {
  return (
    <div className="flex flex-col items-center gap-2">
      <div className="h-8 w-8 animate-spin rounded-full border-2 border-white border-t-transparent" />
      <span className="text-sm text-gray-400">Initializing terminal...</span>
    </div>
  )
}

createDynamicTerminal(importFn, { 
  rows: 15, 
  loading: <CustomLoader />
})

Complete Example

components/my-terminal.tsx
'use client'

import { InkTerminalBox, Box, Text } from 'ink-web'
import 'ink-web/css'
import 'xterm/css/xterm.css'

export function MyTerminal({ onReady }: { onReady?: () => void }) {
  return (
    <InkTerminalBox rows={15} focus onReady={onReady}>
      <Box flexDirection="column">
        <Text color="green">Hello from Ink!</Text>
      </Box>
    </InkTerminalBox>
  )
}
components/terminal-wrapper.tsx
'use client'

import { createDynamicTerminal } from 'ink-web/next'

const Terminal = createDynamicTerminal(
  () => import('./my-terminal').then(m => m.MyTerminal),
  { 
    rows: 15,
    loading: { type: 'skeleton', position: 'top-left' }
  }
)

export function TerminalWrapper() {
  return <Terminal />
}
app/page.tsx
import { TerminalWrapper } from '@/components/terminal-wrapper'

export default function Page() {
  return (
    <div className="p-8">
      <h1>My App</h1>
      <TerminalWrapper />
    </div>
  )
}

How It Works

The createDynamicTerminal helper:

  1. Uses Next.js dynamic() with ssr: false
  2. Renders an InkTerminalLoadingPlaceholder with your chosen loading state
  3. Keeps the skeleton visible until the terminal calls onReady
  4. Ensures no animation reset during React hydration

This provides a seamless loading experience without layout shifts or flickering.

Using Without the Helper

If you need more control, you can use the loading placeholder component directly:

'use client'

import dynamic from 'next/dynamic'
import { InkTerminalLoadingPlaceholder } from 'ink-web/utils'
import { useState } from 'react'

const MyTerminal = dynamic(
  () => import('./my-terminal').then(m => m.MyTerminal),
  { ssr: false }
)

export function TerminalWrapper() {
  const [ready, setReady] = useState(false)
  
  return (
    <div style={{ position: 'relative' }}>
      {!ready && <InkTerminalLoadingPlaceholder rows={15} loading="skeleton" />}
      <div style={{ 
        visibility: ready ? 'visible' : 'hidden',
        position: ready ? 'relative' : 'absolute',
        top: 0, left: 0, right: 0 
      }}>
        <MyTerminal onReady={() => setReady(true)} />
      </div>
    </div>
  )
}