Date: 2026-02-28
You can embed a terminal UI in an Expo app, but it's emulated — iOS/Android sandboxing prevents spawning local shell processes. For a real shell, you need a server-side backend (tmux over WebSocket is a great fit). Three approaches exist, from simplest to most capable.
Use "use dom" to embed xterm.js directly — runs as a webview on native, as-is on web. Zero extra native dependencies.
// components/Terminal.tsx
"use dom";
import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import { useEffect, useRef } from "react";
export default function TerminalView({ onData }: { onData?: (data: string) => void }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const term = new Terminal();
term.open(ref.current!);
term.onData((data) => onData?.(data));
return () => term.dispose();
}, []);
return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
}@fressh/react-native-xtermjs-webview wraps xterm.js inside react-native-webview. Works in Expo managed workflow.
import XTermWebView from '@fressh/react-native-xtermjs-webview';
<XTermWebView
onInput={(data) => { /* send to backend via websocket */ }}
style={{ flex: 1 }}
/>react-native-terminal-component — fully JS, in-memory filesystem, simulated unix commands. Good for toy terminals, game UI, or in-app command palettes. Last updated 2019.
For a real shell experience, connect the xterm.js frontend to a tmux session on a server.
[xterm.js in app] <--WebSocket--> [server] <--PTY--> [tmux attach]
ttyd is a single binary that exposes a terminal over WebSocket+HTTP:
ttyd -W tmux new -A -s mobilePoint xterm.js WebSocket at wss://yourserver:7681/ws. -W enables write, -A attaches or creates.
import { WebSocketServer } from "ws";
import * as pty from "node-pty";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => {
const proc = pty.spawn("tmux", ["new", "-A", "-s", "mobile"], {
cols: 80, rows: 24,
});
proc.onData((data) => ws.send(data));
ws.on("message", (msg) => proc.write(msg.toString()));
ws.on("close", () => proc.kill());
});- Session persistence — disconnect, reconnect later, state preserved
- Resize handling — send
tmux resize-windowwhen the terminal view resizes - Multiple clients — attach from app and desktop simultaneously
- Scripting —
tmux send-keyslets the server inject commands programmatically
No local shell on device. iOS and Android sandbox prevents exec/spawn. Options for "real" terminal:
- Remote shell: WebSocket to server running node-pty/ttyd
- Local WASM shell: WebContainers (web only) or wasm-based shell
- Command emulation: Custom JS command handlers