Terminals and code blocks need to be readable, beautiful, and functional.
Terminals and code blocks need to be readable, beautiful, and functional.
Terminals are where developers feel at home. Make them beautiful with Zen Mono.
.terminal {
background: #0d1117; /* GitHub dark background */
color: #c9d1d9;
font-family: 'Zen Mono', monospace;
font-size: 14px;
line-height: 1.5;
padding: 1rem;
border-radius: 8px;
overflow: auto;
}
.terminal-prompt {
color: #58a6ff; /* Bright blue */
user-select: none;
}
.terminal-prompt::before {
content: '$ ';
color: #8b949e; /* Muted gray */
}
.terminal-output {
white-space: pre-wrap;
word-break: break-word;
}
Support standard ANSI escape codes for colored terminal output.
// ANSI color mapping (standard 16 colors)
const ansiColors = {
// Normal colors (30-37)
30: '#24292f', // black
31: '#cf222e', // red
32: '#1a7f37', // green
33: '#9a6700', // yellow
34: '#0969da', // blue
35: '#8250df', // magenta
36: '#1b7c83', // cyan
37: '#6e7781', // white
// Bright colors (90-97)
90: '#57606a', // bright black (gray)
91: '#ff6b6b', // bright red
92: '#4ade80', // bright green
93: '#fbbf24', // bright yellow
94: '#60a5fa', // bright blue
95: '#c084fc', // bright magenta
96: '#22d3ee', // bright cyan
97: '#f5f5f5', // bright white
};
function parseAnsi(text: string): React.ReactNode[] {
const ansiRegex = /\x1b\[([0-9;]+)m/g;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
let currentColor: string | null = null;
let match;
while ((match = ansiRegex.exec(text)) !== null) {
// Add text before this code
if (match.index > lastIndex) {
const textPart = text.slice(lastIndex, match.index);
parts.push(
currentColor ? (
<span key={parts.length} style={{ color: currentColor }}>
{textPart}
</span>
) : (
textPart
)
);
}
// Parse color code
const codes = match[1].split(';').map(Number);
if (codes[0] === 0) {
currentColor = null; // reset
} else if (ansiColors[codes[0]]) {
currentColor = ansiColors[codes[0]];
}
lastIndex = match.index + match[0].length;
}
// Add remaining text
if (lastIndex < text.length) {
const textPart = text.slice(lastIndex);
parts.push(
currentColor ? (
<span key={parts.length} style={{ color: currentColor }}>
{textPart}
</span>
) : (
textPart
)
);
}
return parts;
}
Essential features:
function Terminal() {
const [history, setHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
const [input, setInput] = useState('');
const [output, setOutput] = useState<string[]>([]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex < history.length - 1) {
const newIndex = historyIndex + 1;
setHistoryIndex(newIndex);
setInput(history[history.length - 1 - newIndex]);
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex > 0) {
const newIndex = historyIndex - 1;
setHistoryIndex(newIndex);
setInput(history[history.length - 1 - newIndex]);
} else if (historyIndex === 0) {
setHistoryIndex(-1);
setInput('');
}
} else if (e.key === 'Enter') {
handleCommand(input);
setHistory([...history, input]);
setHistoryIndex(-1);
setInput('');
}
};
return (
<div className="terminal">
<div className="terminal-output">
{output.map((line, i) => (
<div key={i}>{parseAnsi(line)}</div>
))}
</div>
<div className="terminal-input-line">
<span className="terminal-prompt">$</span>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
className="terminal-input"
/>
</div>
</div>
);
}
Three style options:
.terminal-clean {
background: #0d1117;
/* No effects, perfect clarity */
}
.terminal-retro {
background: #0d1117;
text-shadow: 0 0 2px currentColor;
background-image:
repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
}
Use GitHub, Dracula, Nord, or other popular terminal themes.
Use Shiki for server-side syntax highlighting with TextMate grammars.
import { codeToHtml } from 'shiki';
async function highlightCode(code: string, lang: string) {
return await codeToHtml(code, {
lang,
theme: 'github-dark', // or 'github-light', 'dracula', 'nord', etc.
});
}
// In component
<div dangerouslySetInnerHTML={{ __html: highlightedCode }} />
.code-block {
background: #0d1117;
border-radius: 8px;
overflow: hidden;
margin: 1.5rem 0;
box-shadow: var(--shadow-lg);
font-family: 'Zen Mono', monospace;
}
.code-header {
background: #161b22;
padding: 0.75rem 1rem;
border-bottom: 1px solid #30363d;
display: flex;
justify-content: space-between;
align-items: center;
}
.code-language {
font-size: 0.75rem;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.code-content {
padding: 1rem;
overflow-x: auto;
font-size: 14px;
line-height: 1.6;
}
/* Line numbers */
.code-line {
display: table-row;
}
.code-line-number {
display: table-cell;
padding-right: 1rem;
color: #6e7781;
text-align: right;
user-select: none;
width: 1%;
}
.code-line-content {
display: table-cell;
}
function CopyButton({ code }: { code: string }) {
const [copied, setCopied] = useState(false);
const copyCode = async () => {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<button
onClick={copyCode}
className="copy-button"
aria-label="Copy code"
>
{copied ? <Check size={16} /> : <Copy size={16} />}
{copied ? 'Copied!' : 'Copy'}
</button>
);
}
Highlight specific lines for emphasis:
.code-line-highlighted {
background: rgba(255, 255, 0, 0.1);
border-left: 3px solid #f9c74f;
padding-left: calc(1rem - 3px);
}
Go beyond syntax: highlight meaning.
Use cases:
// Using tree-sitter or LSP for semantic analysis
function applySemanticHighlighting(tokens: Token[]) {
return tokens.map(token => {
if (token.type === 'variable.unused') {
return {
...token,
style: 'opacity: 0.5; text-decoration: line-through;'
};
}
if (token.type === 'function.definition') {
return {
...token,
style: 'font-weight: 600;'
};
}
if (token.type === 'variable.parameter') {
return {
...token,
style: 'font-style: italic;'
};
}
return token;
});
}