Claude-skills tui-designer
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.
git clone https://github.com/ckorhonen/claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/ckorhonen/claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/tui-designer" ~/.claude/skills/ckorhonen-claude-skills-tui-designer && rm -rf "$T"
skills/tui-designer/SKILL.mdTUI Designer - Retro/Cyberpunk Terminal UI Expert
Expert guidance for designing and implementing text-based user interfaces with authentic retro computing aesthetics: CRT monitors, phosphor glow, scanlines, and cyberpunk neon.
When to Use This Skill
Use this skill when:
- Creating terminal-style or hacker aesthetic UIs
- Implementing CRT monitor effects (scanlines, glow, barrel distortion)
- Building cyberpunk/synthwave/retrowave interfaces
- Using Tuimorphic components in React
- Implementing Metal shaders for retro effects in SwiftUI
- Designing neon glow text and UI elements
- Choosing color palettes for retro computing aesthetics
- Working with monospace fonts and box-drawing characters
Design Principles
The Retro/Cyberpunk Aesthetic
This aesthetic draws from:
- CRT monitors: Phosphor glow, scanlines, screen curvature, flicker
- Terminal interfaces: Monospace fonts, box-drawing characters, green/amber text
- Cyberpunk fiction: Neon colors, dark backgrounds, high contrast
- 1980s computing: ASCII art, limited color palettes, blocky graphics
Visual Elements Checklist
- Dark background (near-black with subtle color tint)
- Monospace typography throughout
- Box-drawing characters for borders and frames
- Neon glow on text and/or borders
- Scanline effect (subtle horizontal lines)
- Limited color palette (1-3 accent colors)
- High contrast between text and background
- Optional: CRT curvature, chromatic aberration, flicker
Quick Reference: Color Palettes
Phosphor Green (Classic Terminal)
| Role | Hex | Usage |
|---|---|---|
| Bright | | Primary text, highlights |
| Medium | | Secondary text |
| Dark | | Dimmed elements |
| Background | | Main background |
| Deep BG | | Panel backgrounds |
Cyberpunk Neon
| Role | Hex | Usage |
|---|---|---|
| Cyan | | Primary accent |
| Magenta | | Secondary accent |
| Electric Blue | | Tertiary |
| Hot Pink | | Warnings |
| Background | | Main background |
Amber CRT
| Role | Hex | Usage |
|---|---|---|
| Bright | | Primary text |
| Medium | | Secondary |
| Dark | | Dimmed |
| Background | | Main background |
See color-palettes.md for complete specifications.
Typography
Recommended Fonts
Web:
font-family: 'GNU Unifont', 'IBM Plex Mono', 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
SwiftUI:
.font(.system(size: 14, weight: .regular, design: .monospaced))
Box-Drawing Characters
Light: ─ │ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼ Heavy: ━ ┃ ┏ ┓ ┗ ┛ ┣ ┫ ┳ ┻ ╋ Double: ═ ║ ╔ ╗ ╚ ╝ ╠ ╣ ╦ ╩ ╬ Rounded: ╭ ╮ ╰ ╯
See typography-guide.md for complete reference.
Copywriting
Terminal interfaces have a distinct voice: terse, technical, authoritative. Use these defaults unless the project specifies otherwise.
Core Principles
- Terse and Direct - Every word earns its place
- Technical Authority - The system knows what it's doing
- Mechanical Precision - No hedging, apologies, or filler
Text Formatting
| Element | Case | Example |
|---|---|---|
| Headers/Titles | UPPERCASE | |
| Labels | UPPERCASE | |
| Status indicators | UPPERCASE | , |
| Commands/Input | lowercase | |
| Body text | Sentence case | |
Message Prefixes
[SYS] System message [ERR] Error [USR] User action [WRN] Warning [INF] Information [NET] Network
Vocabulary Quick Reference
| 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 |
Common Patterns
> 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]:
Avoid
- "Please", "Sorry", "Oops"
- "Just", "Maybe", "Might"
- Excessive exclamation points
- Emoji (unless specifically requested)
See copywriting-guide.md for complete voice and tone reference.
Platform: React with Tuimorphic
Tuimorphic is a React component library providing 37 terminal-styled, accessible UI components.
Quick Start
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> ); }
Theme Configuration
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
Key Components
| Component | Usage |
|---|---|
| Actions with |
| Text input with terminal styling |
| Container with box-drawing borders |
| Modal dialogs |
| Dropdown menus |
| Syntax-highlighted code |
| Data tables |
| Tabbed navigation |
| File tree display |
See tuimorphic-reference.md for complete API.
Adding Neon Glow
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; }
Platform: SwiftUI with Metal Shaders
iOS 17+ supports Metal shaders directly in SwiftUI via
.colorEffect(), .distortionEffect(), and .layerEffect().
CRT Effect Implementation
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)
Neon Glow in SwiftUI
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.
Platform: CSS/Vanilla Web
Scanlines Overlay
.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 Effect
.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 Curvature (CSS Transform)
.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); }
WebGL CRT with CRTFilter.js
<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.
Effect Patterns
Scanlines
| Platform | Implementation |
|---|---|
| CSS | pseudo-element |
| SwiftUI | Metal shader with |
| WebGL | Fragment shader brightness modulation |
Bloom/Glow
| Platform | Implementation |
|---|---|
| CSS | Multiple with increasing blur |
| SwiftUI | Multiple modifiers |
| WebGL | Gaussian blur pass + additive blend |
| Three.js | with |
Chromatic Aberration
| 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 |
Flicker
| Platform | Implementation |
|---|---|
| CSS | animation varying opacity 0.9-1.0 |
| SwiftUI | Timer-driven opacity or shader time-based |
| WebGL | Time-based noise multiplier |
Performance Considerations
CSS Effects
andbox-shadow
are GPU-accelerated but expensive with many layerstext-shadow- Limit to 4-5 shadow layers for glow effects
- Use
for animated elementswill-change: transform - Consider
media queryprefers-reduced-motion
SwiftUI/Metal
- Metal shaders run at 60-120fps on all devices supporting iOS 17+
processes every pixel - keep shaders simple.layerEffect()- Pre-compile shaders with
(iOS 18+).compile() - Test on older devices (A12 minimum for iOS 17)
WebGL
- CRTFilter.js uses hardware acceleration, minimal performance impact
- Bloom effects in Three.js require render-to-texture passes
- Consider lower resolution render targets for mobile
General
- Reduce effect intensity on mobile devices
- Provide option to disable effects for accessibility
- Profile with actual content, not just empty screens
Templates
Ready-to-use starter files:
- react-tuimorphic-starter.tsx - React app with Tuimorphic
- swiftui-crt-view.swift - SwiftUI view with CRT effect
- crt-shader.metal - Complete Metal shader
- neon-glow.css - CSS neon effects
Common Pitfalls
This section documents real failure modes and edge cases that break TUI implementations. These are non-obvious gotchas discovered through practical use across platforms.
Terminal Compatibility Issues
Problem: Escape sequences vary across terminal emulators. What works in iTerm2 may fail in older xterm, SSH terminals, or Windows Terminal.
Common failures:
- 256-color vs ANSI: Terminals support different color depths. Code using
hex colors works in modern terminals but falls back to 16-color ANSI on older systems.#rgb - Reset sequences: Forgetting to reset text attributes (
or\x1b[0m
) can poison the terminal state for subsequent output.\x1b[m - Unicode box-drawing on Windows: Windows PowerShell/Terminal may not render
correctly. Test on Windows before shipping.─ │ ┌ ┐ - VT100 incompleteness: SSH to older Linux systems often hits VT100-only support. Box-drawing characters render as gibberish.
Prevention:
- Test on target terminals: iTerm2, Terminal.app, Windows Terminal, SSH Linux, Alacritty
- Fall back to ASCII when box-drawing fails: use
,+
,-
as fallback| - Always reset attributes after styled text:
is your friend\x1b[0m - Detect terminal capability: Check
env var (TERM
vsTERM=xterm-256color
)TERM=xterm
Example safe wrapper:
const supportsUnicode = process.env.TERM && process.env.TERM.includes('256'); const border = supportsUnicode ? '─' : '-';
Layout Breaks on Resize
Problem: Fixed-width layouts collapse when terminal resizes. Common in web-based TUI where aspect ratio changes unexpectedly.
Common failures:
- Hardcoded line widths: A centered header assuming 80-column width breaks on a 60-column terminal.
- Overflow hidden without truncation: Content that's 100% width + padding exceeds container, causing text to spill or wrap unexpectedly.
- Aspect ratio assumptions: Scanline overlays, CRT curves assume 16:9. On ultra-wide or mobile, artifacts become obvious.
- Grid layouts with fixed items: Using
withdisplay: grid
fails to adapt to small screens.grid-template-columns: 100px 200px 100px
Prevention:
- Use viewport-relative units:
,vw
, orvhcalc(100% - Xpx) - Implement text truncation with ellipsis:
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - Test at: 120 columns, 80 columns, 40 columns (mobile), ultra-wide
- For scanline effects, use percentage-based frequency that scales with viewport
- Use CSS
or JS@media
to adapt layout dynamicallyResizeObserver
Scanline 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));
Input Handling Edge Cases
Problem: User input isn't just alphanumeric. Real-world input includes multi-byte UTF-8, special keys, paste buffers, and autocomplete.
Common failures:
- UTF-8 character counting: JavaScript's
counts UTF-16 code units, not characters. A 4-byte emojistring.length
has🎮
, breaking cursor positioning and validation.length: 2 - Paste buffer handling: Users paste large blocks of text. Input handlers without streaming cause UI to freeze.
- Arrow keys vs WASD: Terminal-based games assume WASD but users expect arrow keys. Missing support = bad UX.
- Special keys lost in translation: Ctrl+C, Ctrl+Z, Tab work differently across platforms. SSH terminals lose some meta-keys.
- IME composition: Input Method Engine (for Chinese, Japanese, Korean) sends partial characters before final composition. Validating on every keystroke breaks IME input.
Prevention:
- Use grapheme-aware libraries:
(modern), orIntl.Segmenter
npm packagegraphemesplitter - Implement paste as multi-character stream, not a single event
- Support both arrow keys AND WASD for navigation
- Handle IME: validate only on final
, notcompositionendcompositionupdate - Test on macOS (Command key), Windows (Alt), Linux (various WM behavior)
Correct 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
Performance Issues
Problem: TUI effects (scanlines, glow, flicker) are expensive. Naive implementations cause jank and CPU/battery drain.
Common failures:
- Continuous redraws: Updating DOM every frame via JS (not GPU) causes 60+ reflows per second. Leads to choppy animation, 100%+ CPU.
- Too many text-shadow layers: Each
requires a separate GPU pass. 10+ shadows = measurable lag.text-shadow: 0 0 5px, 0 0 10px, 0 0 20px, ... - Flickering at high frequency:
for flicker ties to animation frame time, causing stutter if main thread is busy.setInterval(..., 16ms) - Scanline overlay recompute: Recalculating
on every resize (without debounce) causes jank.repeating-linear-gradient - WebGL without throttling: Full-screen CRT shaders running at 120fps on low-end mobile = instant battery drain.
Prevention:
- Use CSS
for continuous effects (GPU-accelerated)@keyframes - Limit neon glow to 3-4 text-shadow layers; test on mobile
- Debounce resize handlers:
with throttle, orResizeObserverwindow.requestAnimationFrame - Use
sparingly on elements that animatewill-change: transform, filter - Provide reduced-motion option:
prefers-reduced-motion: reduce - On mobile, detect CPU capacity: throttle effects on low-power devices
Flicker 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);
Accessibility Gaps
Problem: Terminal UIs often ignore accessibility. Screen readers, keyboard navigation, color contrast all suffer.
Common failures:
- No keyboard navigation: Clickable elements only respond to mouse. Screen reader users and keyboard-only users are blocked.
- Color-only status indicators: Red text for error, green for success. Colorblind users (8% of males, 0.5% of females) can't distinguish.
- Missing ARIA labels: Screen readers announce "Button" instead of "Execute command". Confusing for blind users.
- Fixed-size text: Neon glow at 48px looks cool but breaks at 14px for visually impaired users. No zoom support = inaccessible.
- No focus indicators: After Tab-navigating to a button, there's no visible focus ring. Users lose their place.
- Animated elements without pause: Flicker and scanlines can trigger photosensitive epilepsy. Missing
support = legal/safety risk.prefers-reduced-motion
Prevention:
- Implement full keyboard navigation: Tab/Shift+Tab, Enter to activate, Arrow keys for lists
- Test with screen reader: VoiceOver (macOS), NVDA (Windows), JAWS
- Add ARIA labels:
,aria-label="Start system"aria-describedby="help-text" - Ensure 4.5:1 contrast ratio for text (WCAG AA standard)
- Provide
alternative: disable flicker, scanlines, animationsprefers-reduced-motion - Focus indicators:
is mandatory:focus { outline: 2px solid #0ff; }
Accessible 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>
Terminal Emulator Differences
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 |
| SSH/xterm | VT100 only; no true-color; no mouse events | Fall back to ASCII + ANSI colors |
| Tmux | Passthrough mode required for true-color: | 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
When CRT Effects Backfire
Problem: Scanlines, glow, and distortion look cool in mockups but cause problems in practice.
Common failures:
- Scanlines reduce readability: Fine horizontal lines over text make it harder to read, especially on mobile or at distance. People with low vision can't read it.
- Glow hides text: Heavy neon glow (
with 10+ layers) blurs text edges, reducing legibility.text-shadow - Barrel distortion breaks alignment: CRT curvature looks authentic but makes content appear misaligned. Users think UI is broken.
- Flicker induces motion sickness: Subtle flicker causes nausea in some users. Photosensitive epilepsy is a real safety concern.
Prevention:
- Use subtle scanlines: 10-15% opacity, visible but not interfering with text
- Limit glow intensity: if glow radius > 20px, text becomes unreadable
- Distortion is decorative only: never use it for critical layout
- Always provide
escape hatchprefers-reduced-motion - Test with actual users: "Can you read this comfortably?"
Resources
Libraries
- Tuimorphic - React terminal UI components
- Inferno - Metal shaders for SwiftUI
- CRTFilter.js - WebGL CRT effects
- @react-three/postprocessing - React Three.js effects
Documentation
Inspiration
- SRCL / Sacred Computer - Design influence for Tuimorphic
- Cool Retro Term - Terminal emulator with CRT effects