Welcome! Type "help" for available commands.
$
Loading terminal interface...
Back to Blog

Building an Interactive Terminal in Next.js

April 7, 2025
William Callahan

Software engineer and founder with a background in finance and tech. Currently building aVenture.vc, a platform for researching private companies. Based in San Francisco.

nextjsreacttypescripthooksstate managementapp routerhydrationcontextuiprismjsdark mode
Building an Interactive Terminal in Next.js

Overview: Goal & Key Challenges

Building an embedded terminal UI with macOS-style window controls in Next.js (App Router). Key challenge: managing shared window state across components/routes without hydration errors using React Context.

1: Component Architecture

Key components and their roles:

Toggle dropdown1.1: Global State Provider
  • Role: Central React Context placed in the root layout (app/layout.tsx) to manage state for all registered window components.
Toggle dropdown1.2: Feature Client Components
  • Role: Represent features/pages (e.g., Blog) designed to behave like independent, controllable windows.
  • Integration: Use a custom useRegisteredWindowState hook to connect to the global provider, accessing their specific state (windowState) and control functions (minimize, maximize, etc.).
Toggle dropdown1.3: Floating Restore Buttons
  • Role: Reads the global state to display buttons allowing users to restore minimized or closed windows.
Toggle dropdown1.4: Terminal Component
  • Role: The interactive terminal UI itself, also registered as a window via the useRegisteredWindowState hook for consistent control.
Toggle dropdown1.5: Window Control Buttons
  • Role: Provides standard macOS-style UI buttons (close/minimize/maximize).
  • Integration: A simple UI component accepting onClose, onMinimize, onMaximize callbacks provided by its parent window component (which obtains them from the registration hook).

2: Window State Management

Managing multiple window states across pages required a global React Context pattern to avoid hydration issues common with local state management.

Problem: Local State & Hydration Conflicts

Component-local state (even with localStorage) caused server-client UI mismatches (hydration errors) and was complex for multiple windows. Solution: A global registry via React Context.

Toggle dropdown2.1: Global Context Details
  • Uses React.createContext to define a central store.
  • Holds a record mapping unique window IDs to their information (id, icon, title, state).
  • Provides access to the shared windows state object and management functions (registerWindow, setWindowState, etc.).
Toggle dropdown2.2: Provider Implementation
  • The Context Provider component wraps the RootLayout in app/layout.tsx, making the state available application-wide.
  • Internally uses useState to hold and update the central windows record.
  • A client-side useEffect hook conditionally adds CSS classes (window-maximized, window-minimized) to the <body> tag based on the overall window state. This allows global CSS to adjust shared layout elements (like headers) without complex prop drilling.
// Simplified app/layout.tsx structure
import { GlobalWindowRegistryProvider } from "@/lib/context/GlobalWindowRegistryContext";
// ... other imports

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Providers>
          <GlobalWindowRegistryProvider>
            {/* ... layout elements ... */}
            <main>
              {/* Registered components like ClientTerminal */}
              {children}
            </main>
            <FloatingRestoreButtons />
          </GlobalWindowRegistryProvider>
        </Providers>
      </body>
    </html>
  );
}
Toggle dropdown2.3: Registration Hook
  • Purpose: Allows individual components (acting as windows) to register with the global context, retrieve their current state, and receive functions to modify that state.
  • Mechanism: A custom hook that encapsulates the logic for consuming the context and handling the registration/unregistration lifecycle automatically using useEffect.
  • Returns: Provides the component with its specific windowState, memoized action functions (minimize, maximize, close, restore), and a required isRegistered boolean flag to ensure rendering only occurs safely after client-side hydration and registration is complete.

Key Implementation Points

  • Hydration Safety: Always check the isRegistered flag from the hook before rendering state-dependent UI to prevent server/client mismatches. Render default/loading state until isRegistered is true.
  • Layout Control: Each window component is responsible for its own layout styling (e.g., position: fixed, dimensions, z-index) based on its windowState.
  • Minimized/Closed State: When minimized or closed, components typically render null. The separate FloatingRestoreButtons component handles the UI for restoring them.
  • Targeted Global CSS: Use body.window-maximized / body.window-minimized classes sparingly to adjust shared elements (like site headers) without interfering with individual window component layouts.

3: Server/Client Data Flow

Passing non-serializable data (like JSX elements or functions) directly from Server Components to Client Components causes errors in Next.js.

Problem: Non-Serializable Data Across Boundaries

Data crossing the Server-Client boundary must be JSON-serializable. Complex objects, functions, or modules required on the server (like fs) cannot be passed directly, leading to client-side errors or broken Hot Module Replacement (HMR).

Toggle dropdownSolution: Keep Server Logic Server-Side

The reliable pattern involves strict separation:

  1. Isolate Server Logic: Create utility functions marked with "use server" or using import "server-only". These handle server-specific tasks (DB access, API calls, file reads) and process data into simple, serializable formats (plain objects/arrays).
  2. Orchestrate in Server Component: The main Server Component (e.g., a page) calls these utilities, gathers the necessary data, and ensures it's fully processed into a serializable structure.
  3. Pass Clean Data: Pass only the processed, serializable data as props to the top-level Client Component wrapper for the feature.
  4. Render on Client: The Client Component receives the clean data and uses it to render its UI sub-components. This prevents server-only code or non-serializable data from attempting to run on or be sent to the client.

4: Auto-Scrolling Terminal

Implemented using standard React hooks:

Toggle dropdownHow it Works
  • useRef obtains a direct reference to the terminal's scrollable container element.
  • useEffect monitors changes in the command history (managed in a separate TerminalContext).
  • When history updates, the effect programmatically sets the container's scrollTop to its scrollHeight, ensuring the latest output is always visible.

5: Command Handling

Command processing logic is centralized for maintainability:

Toggle dropdownHow Commands Are Processed
  • User input is captured by the terminal component.
  • A useTerminal hook takes the submitted command string.
  • The hook passes the command to a central, asynchronous handleCommand function.
  • handleCommand is responsible for:
    • Parsing the input (command and arguments).
    • Matching against predefined commands or known application routes.
    • Performing the appropriate action—returning formatted text output, calling separate search logic, using Next.js's useRouter for client-side navigation, or returning structured error messages.

6: Syntax-Highlighted Code Blocks with PrismJS

Creating responsive, syntax-highlighted code blocks with proper dark mode support required solving several CSS conflicts.

Challenge: Dark Mode Compatibility

PrismJS's default themes assume a fixed color scheme. Making syntax highlighting work in both light and dark modes while maintaining consistent UI required custom CSS overrides and DOM structure simplification.

Toggle dropdown6.1: Implementation Approach
  • Base Implementation: PrismJS handles the syntax tokenization and adds language-specific classes to code elements.
  • Theme Integration: Import a basic PrismJS theme in the root layout:
// app/layout.tsx
import './globals.css';
import './code-blocks.css';
import '../components/ui/code-block/prism-syntax-highlighting/prism.css';
  • Component Structure: A custom CodeBlock React component provides the macOS-style window UI around the pre/code elements.
  • Custom Theme Overrides: Dark mode specific styles in code-blocks.css override PrismJS's default theme for dark appearances.
Toggle dropdown6.2: Solving Dark Mode Text Highlighting

One persistent issue was text shadow/highlighting in dark mode. PrismJS's default styling includes text shadows and background colors that conflicted with our dark theme.

The solution required multiple CSS overrides:

/* Force remove backgrounds and text shadows from ALL tokens */
pre[class*="language-"] *,
code[class*="language-"] * {
  text-shadow: none !important;
  background: transparent !important;
  box-shadow: none !important;
}

/* Override PrismJS token styles for dark mode */
.dark .token.comment,
.dark .token.string,
.dark .token.function /* etc... */ {
  color: #specifc-color; /* Color-only syntax highlighting */
  background: transparent !important;
}

These rules ensure tokens are styled with color only, not backgrounds or shadows.

Toggle dropdown6.3: Fixing Rounded Corner Rendering

Another challenge was border radius rendering artifacts. The complex nesting of elements with borders and backgrounds created visual glitches at the rounded corners.

The solution was to simplify the DOM structure:

<div className="rounded-lg overflow-hidden"> {/* Single container with rounded corners */}
  {/* Header */}
  <div className="rounded-t-lg">
    <WindowControls />
  </div>

  {/* Content */}
  <div>
    <pre className="...">
      {children}
    </pre>
  </div>
</div>

This flatter structure avoids border conflicts and ensures clean rounded corners.

Toggle dropdown6.4: CSS @apply Issues

Using Tailwind's @apply directive in global CSS caused linting errors. Replace:

/* Problematic */
:not(pre) > code {
  @apply bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-medium;
}

/* Solution - standard CSS */
:not(pre) > code {
  background-color: #f3f4f6; /* bg-gray-100 */
  color: #111827; /* text-gray-900 */
  padding: 0.125rem 0.375rem; /* px-1.5 py-0.5 */
  border-radius: 0.25rem; /* rounded */
  /* etc. */
}

.dark :not(pre) > code {
  background-color: #1f2937; /* dark:bg-gray-800 */
  color: #f9fafb; /* dark:text-gray-50 */
}

Summary

  • Global State: React Context in the root layout is effective for managing shared UI state (like window status) across different routes in the Next.js App Router.
  • Hydration: A custom registration hook (like useRegisteredWindowState) combined with an isRegistered flag helps safely manage client-side state dependent on context, preventing hydration errors.
  • Data Flow: Maintain strict separation between server-side data fetching/processing and client-side rendering. Pass only serializable data from Server to Client Components.
  • Syntax Highlighting: When integrating libraries like PrismJS, focus on simplifying DOM structure and providing explicit theme overrides for dark mode compatibility.
  • CSS Complexity: Use !important flags judiciously when working with third-party CSS that conflicts with your theme. Prefer explicit selectors over complex nesting that can lead to specificity wars.

Similar Content

Home
CV
ExperienceEducation
ProjectsBookmarksInvestmentsContactBlog
Welcome! Type "help" for available commands.
$
Loading terminal interface...

Similar Content

Related Articles

April 2, 2025
Adding a GitHub Contribution Graph to Next.js

Adding a GitHub Contribution Graph to Next.js

How to add a GitHub contribution graph to your Next.js site using GitHub's GraphQL API, with server-side caching.

nextjsgithubgraphqlapireacttypescript+9
BLOG

Related Bookmarks

v2.tauri.app
May 3, 2025
Tauri 2.0

Tauri 2.0

The cross-platform app building toolkit

frontend frameworkscross-platform app developmentrust programmingapp securitytauribuilding+1
LINK
simonwillison.net
December 11, 2025
Useful patterns for building HTML tools

Useful patterns for building HTML tools

I’ve started using the term HTML tools to refer to HTML applications that I’ve been building which combine HTML, JavaScript, and CSS in a single file ...

javascript utilitieshtml toolssingle page applicationsllm generated codeweb development workflowspatterns+7
LINK
mariozechner.at
December 13, 2025
What I learned building an opinionated and minimal coding agent

What I learned building an opinionated and minimal coding agent

Lessons I learned while building my own coding agent from scratch.

ai coding agentsdeveloper productivity toolsterminal user interfacesllm tooling and frameworksself-hosted llmsagent+7
LINK

Related Projects

williamcallahan.com

williamcallahan.com

Interactive personal site with beautiful terminal/code components & other dynamic content

graph indexs3 object storageinteractive appterminal uimdx blogsearch+8
PRJ
SearchAI

SearchAI

AI-powered web search with a contextual chat assistant

aiweb searchchat assistantopenaigptrag+11
PRJ

Related Books

Build AI Applications with Spring AI

Build AI Applications with Spring AI

Fu Cheng

fu chengspringbuildapplications
BOOK
React in Depth

React in Depth

Morten Barklund

React in Depthteaches the React libraries, tools and techniques that are vital to build amazing apps. You'll put each skill you learn into practice wi...

computersmorten barklundsimon and schusterreactdepthdepthteaches+5
BOOK
Advanced Algorithms and Data Structures

Advanced Algorithms and Data Structures

Marcello La Rocca

marcello la roccaadvancedalgorithmsdatastructures
BOOK

Related Investments

FlutterFlow

FlutterFlow

aVenture

No-code platform for building native mobile applications using Flutter, enabling rapid app development without coding.

developer toolsseed+activeflutterflowplatformno-code+5
INV
Upstock

Upstock

Equity management platform helping companies create and manage employee equity plans.

investment platformsseedactiveupstockequityplatform+5
INV
Copilot

Copilot

Personal finance app using AI to help young professionals track, understand, and optimize their spending and savings.

financeseedactivecopilotpersonalapp+5
INV