skills/tui-designer/SKILL.md
Design and implement retro/cyberpunk/hacker-style terminal UIs. Covers React (Tuimorphic), SwiftUI (Metal shaders), and CSS approaches. Use when creating terminal aesthetics, CRT effects, neon glow, scanlines, phosphor green displays, or retro-futuristic interfaces.
npx skillsauth add ckorhonen/claude-skills tui-designerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert guidance for designing and implementing text-based user interfaces with authentic retro computing aesthetics: CRT monitors, phosphor glow, scanlines, and cyberpunk neon.
Use this skill when:
This aesthetic draws from:
| Role | Hex | Usage |
|------|-----|-------|
| Bright | #00ff00 | Primary text, highlights |
| Medium | #00cc00 | Secondary text |
| Dark | #009900 | Dimmed elements |
| Background | #001100 | Main background |
| Deep BG | #000800 | Panel backgrounds |
| Role | Hex | Usage |
|------|-----|-------|
| Cyan | #00ffff | Primary accent |
| Magenta | #ff00ff | Secondary accent |
| Electric Blue | #0066ff | Tertiary |
| Hot Pink | #ff1493 | Warnings |
| Background | #0a0a1a | Main background |
| Role | Hex | Usage |
|------|-----|-------|
| Bright | #ffb000 | Primary text |
| Medium | #cc8800 | Secondary |
| Dark | #996600 | Dimmed |
| Background | #1a1000 | Main background |
See color-palettes.md for complete specifications.
Web:
font-family: 'GNU Unifont', 'IBM Plex Mono', 'JetBrains Mono',
'SF Mono', 'Consolas', monospace;
SwiftUI:
.font(.system(size: 14, weight: .regular, design: .monospaced))
Light: ─ │ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼
Heavy: ━ ┃ ┏ ┓ ┗ ┛ ┣ ┫ ┳ ┻ ╋
Double: ═ ║ ╔ ╗ ╚ ╝ ╠ ╣ ╦ ╩ ╬
Rounded: ╭ ╮ ╰ ╯
See typography-guide.md for complete reference.
Terminal interfaces have a distinct voice: terse, technical, authoritative. Use these defaults unless the project specifies otherwise.
| Element | Case | Example |
|---------|------|---------|
| Headers/Titles | UPPERCASE | SYSTEM STATUS |
| Labels | UPPERCASE | CPU USAGE: |
| Status indicators | UPPERCASE | ONLINE, OFFLINE |
| Commands/Input | lowercase | > run diagnostic |
| Body text | Sentence case | Connection established |
[SYS] System message [ERR] Error
[USR] User action [WRN] Warning
[INF] Information [NET] Network
| Action | Terminal Verbs | |--------|---------------| | Start | INITIALIZE, BOOT, LAUNCH, ACTIVATE | | Stop | TERMINATE, HALT, ABORT, KILL | | Save | WRITE, COMMIT, STORE, PERSIST | | Load | READ, FETCH, RETRIEVE, LOAD | | Delete | PURGE, REMOVE, CLEAR, WIPE |
| State | Terminal Words | |-------|---------------| | Working | PROCESSING, EXECUTING, RUNNING | | Done | COMPLETE, SUCCESS, FINISHED | | Failed | ERROR, FAULT, ABORTED | | Ready | ONLINE, AVAILABLE, ARMED |
> INITIALIZING SYSTEM...
> LOADING MODULES [████████░░] 80%
> AUTHENTICATION COMPLETE
> SYSTEM READY
ERROR: ACCESS DENIED
ERR_CONNECTION_REFUSED: Timeout after 30s
WARNING: Low disk space (< 10%)
CONFIRM DELETE? [Y/N]
SELECT OPTION [1-5]:
See copywriting-guide.md for complete voice and tone reference.
Tuimorphic is a React component library providing 37 terminal-styled, accessible UI components.
npm install tuimorphic
import { Button, Card, Input } from 'tuimorphic';
import 'tuimorphic/styles.css';
function App() {
return (
<div className="theme-dark tint-green">
<Card>
<h1>SYSTEM ACCESS</h1>
<Input placeholder="Enter command..." />
<Button variant="primary">EXECUTE</Button>
</Card>
</div>
);
}
Apply themes via CSS classes on a parent element:
// Dark mode with green tint
<div className="theme-dark tint-green">
// Light mode with cyan tint
<div className="theme-light tint-blue">
Available tints: tint-green, tint-blue, tint-red, tint-yellow, tint-purple, tint-orange, tint-pink
| Component | Usage |
|-----------|-------|
| Button | Actions with variant="primary\|secondary\|ghost" |
| Input | Text input with terminal styling |
| Card | Container with box-drawing borders |
| Dialog | Modal dialogs |
| Menu | Dropdown menus |
| CodeBlock | Syntax-highlighted code |
| Table | Data tables |
| Tabs | Tabbed navigation |
| TreeView | File tree display |
See tuimorphic-reference.md for complete API.
Enhance Tuimorphic with CSS glow effects:
/* Neon text glow */
.neon-text {
color: #0ff;
text-shadow:
0 0 5px #fff,
0 0 10px #fff,
0 0 20px #0ff,
0 0 40px #0ff,
0 0 80px #0ff;
}
/* Neon border glow */
.neon-border {
border-color: #0ff;
box-shadow:
0 0 5px #0ff,
0 0 10px #0ff,
inset 0 0 5px #0ff;
}
/* Flickering animation */
@keyframes flicker {
0%, 100% { opacity: 1; }
50% { opacity: 0.95; }
52% { opacity: 1; }
54% { opacity: 0.9; }
}
.flicker {
animation: flicker 3s infinite;
}
iOS 17+ supports Metal shaders directly in SwiftUI via .colorEffect(), .distortionEffect(), and .layerEffect().
CRT.metal:
#include <metal_stdlib>
#include <SwiftUI/SwiftUI.h>
using namespace metal;
[[stitchable]] half4 crtEffect(
float2 position,
SwiftUI::Layer layer,
float time,
float2 size,
float scanlineIntensity,
float distortionStrength
) {
float2 uv = position / size;
// Barrel distortion
float2 center = uv - 0.5;
float dist = length(center);
float2 distorted = center * (1.0 + distortionStrength * dist * dist);
float2 samplePos = (distorted + 0.5) * size;
// Bounds check
if (samplePos.x < 0 || samplePos.x > size.x ||
samplePos.y < 0 || samplePos.y > size.y) {
return half4(0, 0, 0, 1);
}
// Sample color
half4 color = layer.sample(samplePos);
// Scanlines
float scanline = sin(position.y * 3.14159 * 2.0) * scanlineIntensity;
color.rgb *= 1.0 - scanline;
// Subtle color shift (chromatic aberration)
color.r *= 1.0 + 0.02 * sin(time * 2.0);
color.b *= 1.0 - 0.02 * sin(time * 2.0);
// Slight flicker
color.rgb *= 1.0 + 0.01 * sin(time * 60.0);
return color;
}
SwiftUI View Modifier:
struct CRTEffectModifier: ViewModifier {
@State private var startTime = Date()
var scanlineIntensity: Float = 0.1
var distortionStrength: Float = 0.1
func body(content: Content) -> some View {
TimelineView(.animation) { timeline in
let time = Float(timeline.date.timeIntervalSince(startTime))
GeometryReader { geo in
content
.layerEffect(
ShaderLibrary.crtEffect(
.float(time),
.float2(geo.size),
.float(scanlineIntensity),
.float(distortionStrength)
),
maxSampleOffset: .init(width: 10, height: 10)
)
}
}
}
}
extension View {
func crtEffect(
scanlines: Float = 0.1,
distortion: Float = 0.1
) -> some View {
modifier(CRTEffectModifier(
scanlineIntensity: scanlines,
distortionStrength: distortion
))
}
}
Usage:
Text("SYSTEM ONLINE")
.font(.system(size: 24, weight: .bold, design: .monospaced))
.foregroundColor(.green)
.crtEffect(scanlines: 0.15, distortion: 0.08)
extension View {
func neonGlow(color: Color, radius: CGFloat = 10) -> some View {
self
.shadow(color: color.opacity(0.8), radius: radius / 4)
.shadow(color: color.opacity(0.6), radius: radius / 2)
.shadow(color: color.opacity(0.4), radius: radius)
.shadow(color: color.opacity(0.2), radius: radius * 2)
}
}
// Usage
Text("NEON")
.font(.system(size: 48, design: .monospaced))
.foregroundColor(.cyan)
.neonGlow(color: .cyan, radius: 15)
See metal-shaders-ios.md for complete shader code.
.crt-container {
position: relative;
background: #000800;
}
.crt-container::after {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
}
.neon-text {
color: #0ff;
text-shadow:
/* White core */
0 0 5px #fff,
0 0 10px #fff,
/* Colored glow layers */
0 0 20px #0ff,
0 0 30px #0ff,
0 0 40px #0ff,
0 0 55px #0ff,
0 0 75px #0ff;
}
.crt-screen {
border-radius: 20px;
transform: perspective(1000px) rotateX(2deg);
box-shadow:
inset 0 0 50px rgba(0, 255, 0, 0.1),
0 0 20px rgba(0, 255, 0, 0.2);
}
<script type="module">
import { CRTFilterWebGL } from 'crtfilter';
const canvas = document.getElementById('crt-canvas');
const crt = new CRTFilterWebGL(canvas, {
scanlineIntensity: 0.15,
glowBloom: 0.3,
chromaticAberration: 0.002,
barrelDistortion: 0.1,
staticNoise: 0.03,
flicker: true,
retraceLines: true
});
crt.start();
</script>
See crt-effects-web.md for complete techniques.
| Platform | Implementation |
|----------|---------------|
| CSS | repeating-linear-gradient pseudo-element |
| SwiftUI | Metal shader with sin(position.y * frequency) |
| WebGL | Fragment shader brightness modulation |
| Platform | Implementation |
|----------|---------------|
| CSS | Multiple text-shadow with increasing blur |
| SwiftUI | Multiple .shadow() modifiers |
| WebGL | Gaussian blur pass + additive blend |
| Three.js | UnrealBloomPass with luminanceThreshold |
| Platform | Implementation | |----------|---------------| | CSS | Three overlapping elements with color channel offset | | SwiftUI | Sample texture at offset positions per RGB channel | | WebGL | Sample UV with slight offset per channel |
| Platform | Implementation |
|----------|---------------|
| CSS | @keyframes animation varying opacity 0.9-1.0 |
| SwiftUI | Timer-driven opacity or shader time-based |
| WebGL | Time-based noise multiplier |
box-shadow and text-shadow are GPU-accelerated but expensive with many layerswill-change: transform for animated elementsprefers-reduced-motion media query.layerEffect() processes every pixel - keep shaders simple.compile() (iOS 18+)Ready-to-use starter files:
This section documents real failure modes and edge cases that break TUI implementations. These are non-obvious gotchas discovered through practical use across platforms.
Problem: Escape sequences vary across terminal emulators. What works in iTerm2 may fail in older xterm, SSH terminals, or Windows Terminal.
Common failures:
#rgb hex colors works in modern terminals but falls back to 16-color ANSI on older systems.\x1b[0m or \x1b[m) can poison the terminal state for subsequent output.─ │ ┌ ┐ correctly. Test on Windows before shipping.Prevention:
+, -, | as fallback\x1b[0m is your friendTERM env var (TERM=xterm-256color vs TERM=xterm)Example safe wrapper:
const supportsUnicode = process.env.TERM && process.env.TERM.includes('256');
const border = supportsUnicode ? '─' : '-';
Problem: Fixed-width layouts collapse when terminal resizes. Common in web-based TUI where aspect ratio changes unexpectedly.
Common failures:
display: grid with grid-template-columns: 100px 200px 100px fails to adapt to small screens.Prevention:
vw, vh, or calc(100% - Xpx)overflow: hidden; text-overflow: ellipsis; white-space: nowrap;@media or JS ResizeObserver to adapt layout dynamicallyScanline example that fails:
/* ❌ Breaks on narrow screens */
background: repeating-linear-gradient(0deg, transparent, transparent 20px, black 20px, black 21px);
Better approach:
/* ✅ Scales with viewport height */
background: repeating-linear-gradient(0deg, transparent, transparent calc(2% of height), black calc(2% of height), black calc(2% of height + 1px));
Problem: User input isn't just alphanumeric. Real-world input includes multi-byte UTF-8, special keys, paste buffers, and autocomplete.
Common failures:
string.length counts UTF-16 code units, not characters. A 4-byte emoji 🎮 has length: 2, breaking cursor positioning and validation.Prevention:
Intl.Segmenter (modern), or graphemesplitter npm packagecompositionend, not compositionupdateCorrect UTF-8 cursor positioning:
// ❌ Wrong: counts UTF-16 code units
const cursorPos = input.value.length; // "🎮🎮" = length: 4
// ✅ Correct: counts grapheme clusters
const segmenter = new Intl.Segmenter();
const cursorPos = [...segmenter.segment(input.value)].length; // "🎮🎮" = 2
Problem: TUI effects (scanlines, glow, flicker) are expensive. Naive implementations cause jank and CPU/battery drain.
Common failures:
text-shadow: 0 0 5px, 0 0 10px, 0 0 20px, ... requires a separate GPU pass. 10+ shadows = measurable lag.setInterval(..., 16ms) for flicker ties to animation frame time, causing stutter if main thread is busy.repeating-linear-gradient on every resize (without debounce) causes jank.Prevention:
@keyframes for continuous effects (GPU-accelerated)ResizeObserver with throttle, or window.requestAnimationFramewill-change: transform, filter sparingly on elements that animateprefers-reduced-motion: reduceFlicker that performs well:
/* ✅ GPU-accelerated, smooth */
@keyframes flicker {
0%, 100% { opacity: 1; }
50% { opacity: 0.95; }
}
.flicker { animation: flicker 3s infinite; }
Flicker that causes jank:
// ❌ Main thread blocked, stutter
setInterval(() => {
element.style.opacity = Math.random() > 0.5 ? 1 : 0.9;
}, 50);
Problem: Terminal UIs often ignore accessibility. Screen readers, keyboard navigation, color contrast all suffer.
Common failures:
prefers-reduced-motion support = legal/safety risk.Prevention:
aria-label="Start system", aria-describedby="help-text"prefers-reduced-motion alternative: disable flicker, scanlines, animations:focus { outline: 2px solid #0ff; } is mandatoryAccessible neon button:
<button
aria-label="Execute diagnostic"
className="neon-button"
onClick={handleClick}
onKeyDown={(e) => e.key === 'Enter' && handleClick()}
>
EXECUTE
</button>
<style>
.neon-button:focus {
outline: 2px solid #0ff;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.neon-button {
text-shadow: none; /* Remove glow */
animation: none; /* Remove flicker */
}
}
</style>
Quick reference for platform-specific behaviors:
| Emulator | Notable Quirks | Fix |
|----------|---|---|
| iTerm2 (macOS) | Renders all Unicode correctly; true-color support | None needed |
| Terminal.app (macOS) | Limited color palette; older xterm behavior | Test with 256-color mode |
| Alacritty | Ultra-fast; true-color; may not render some Unicode | Works reliably if iTerm2 works |
| Windows Terminal | Supports true-color; WSL integration works well | None needed for modern code |
| PowerShell | Old versions render box-drawing as ?; use fallback | Check $PSVersionTable |
| SSH/xterm | VT100 only; no true-color; no mouse events | Fall back to ASCII + ANSI colors |
| Tmux | Passthrough mode required for true-color: set -g default-terminal "tmux-256color" | Configure tmux.conf |
| Screen | Even older terminal support; avoid fancy effects | Use ASCII-only mode |
Platform detection example:
if [[ "$TERM" == "xterm" ]]; then
# Old terminal: use ASCII
BORDER="+"
COLORS="ANSI"
elif [[ "$TERM" == *"256color"* ]]; then
# Modern terminal: use box-drawing + 256 colors
BORDER="─"
COLORS="256"
else
# Unknown: default to safe
BORDER="+"
COLORS="ANSI"
fi
Problem: Scanlines, glow, and distortion look cool in mockups but cause problems in practice.
Common failures:
text-shadow with 10+ layers) blurs text edges, reducing legibility.Prevention:
prefers-reduced-motion escape hatchdocumentation
Create or expand an Idea.md / IDEA.md file from a rough description, existing repo, conversation history, notes, or other early-stage product inputs. Use when the user asks to "write an Idea.md", "turn this into an idea file", "capture this product idea", "expand this concept", or wants a repo-grounded concept brief before validation, PRD, or implementation work.
development
Write structured implementation plans from specs or requirements before touching code. Use when given a spec, requirements doc, or feature description, when user says "plan this out", "write a plan for", "how should we implement", or before starting any multi-step coding task.
testing
Expert guidance for video editing with ffmpeg, encoding best practices, and quality optimization. Use when working with video files, transcoding, remuxing, encoding settings, color spaces, or troubleshooting video quality issues.
development
Opinionated constraints for building better interfaces with agents. Use when building UI components, implementing animations, designing layouts, reviewing frontend accessibility, or working with Tailwind CSS, motion/react, or accessible primitives like Radix/Base UI.