FlipOff Split-Flap Display Emulator Skill by ara.so — Daily 2026 Skills collection. FlipOff is a pure vanilla HTML/CSS/JS web app that emulates classic mechanical split-flap (flip-board) airport displays. No frameworks, no npm, no build step — open index.html and you have a full-screen retro display with authentic scramble animations and clacking sounds. Installation git clone https://github.com/magnum6actual/flipoff.git cd flipoff
Option 1: Open directly
open index.html
Option 2: Serve locally (recommended for audio)
python3 -m http.server 8080
Visit http://localhost:8080
Audio note: Browsers block autoplay. The user must click once to enable the Web Audio API context. After that, sound plays automatically on each message transition. File Structure flipoff/ index.html — Single-page app entry point css/ reset.css — CSS reset layout.css — Header, hero, page layout board.css — Board container and accent bars tile.css — Tile styling and 3D flip animation responsive.css — Media queries (mobile → 4K) js/ main.js — Entry point, wires everything together Board.js — Grid manager, transition orchestration Tile.js — Per-tile animation logic SoundEngine.js — Web Audio API playback MessageRotator.js — Auto-rotate timer KeyboardController.js — Keyboard shortcut handling constants.js — All configuration lives here flapAudio.js — Base64-encoded audio data Key Configuration — js/constants.js Everything you'd want to change lives in one file: // js/constants.js (representative structure) export const GRID_COLS = 26 ; // Characters per row export const GRID_ROWS = 8 ; // Number of rows export const SCRAMBLE_DURATION = 600 ; // ms each tile scrambles before settling export const STAGGER_DELAY = 18 ; // ms between each tile starting its scramble export const AUTO_ROTATE_INTERVAL = 8000 ; // ms between auto-advancing messages export const SCRAMBLE_COLORS = [ '#FF6B35' , '#F7C59F' , '#EFEFD0' , '#004E89' , '#1A936F' , '#C6E0F5' ] ; export const ACCENT_COLORS = [ '#FF6B35' , '#004E89' , '#1A936F' ] ; export const MESSAGES = [ "HAVE A NICE DAY" , "ALL FLIGHTS ON TIME" , "WELCOME TO THE FUTURE" , // Add your own here ] ; Adding Custom Messages Edit MESSAGES in js/constants.js . Each message is a plain string. The board wraps text across the grid automatically. export const MESSAGES = [ "DEPARTING GATE 7" , "YOUR COFFEE IS READY" , "BUILD THINGS THAT MATTER" , "FLIGHT AA 404 NOT FOUND" , // max GRID_COLS * GRID_ROWS chars ] ; Padding rules: Messages shorter than the grid are padded with spaces. Messages longer than the grid are truncated. Keep messages at or under GRID_COLS × GRID_ROWS characters. Changing Grid Size // Compact 16×4 ticker-style board export const GRID_COLS = 16 ; export const GRID_ROWS = 4 ; // Wide cinema board export const GRID_COLS = 40 ; export const GRID_ROWS = 6 ; // Tall info kiosk export const GRID_COLS = 20 ; export const GRID_ROWS = 12 ; After changing grid dimensions, tiles re-render automatically on next page load. Keyboard Shortcuts Key Action Enter / Space Next message Arrow Left Previous message Arrow Right Next message F Toggle fullscreen M Toggle mute Escape Exit fullscreen Programmatic API Board // Board.js exposes a class you can instantiate directly import Board from './js/Board.js' ; const board = new Board ( document . getElementById ( 'board-container' ) ) ; // Display a specific string board . setMessage ( 'GATE CHANGE B12' ) ; // Advance to next message in the rotation board . next ( ) ; // Go back board . previous ( ) ; MessageRotator import MessageRotator from './js/MessageRotator.js' ; const rotator = new MessageRotator ( board , messages , AUTO_ROTATE_INTERVAL ) ; rotator . start ( ) ; // begin auto-advancing rotator . stop ( ) ; // pause rotation rotator . next ( ) ; // manual advance rotator . previous ( ) ; // manual back SoundEngine import SoundEngine from './js/SoundEngine.js' ; const sound = new SoundEngine ( ) ; // Must call after a user gesture (click/keypress) await sound . init ( ) ; sound . play ( ) ; // play the flap transition sound sound . mute ( ) ; // silence sound . unmute ( ) ; sound . toggle ( ) ; // flip mute state KeyboardController import KeyboardController from './js/KeyboardController.js' ; const kb = new KeyboardController ( { onNext : ( ) => rotator . next ( ) , onPrevious : ( ) => rotator . previous ( ) , onFullscreen : ( ) => toggleFullscreen ( ) , onMute : ( ) => sound . toggle ( ) , } ) ; kb . attach ( ) ; // start listening kb . detach ( ) ; // stop listening Embedding FlipOff in Another Page As an iframe < iframe src = " /flipoff/index.html " width = " 1280 " height = " 400 " style = " border : none ; background :
000
; " allowfullscreen
</ iframe
Inline embed (pull in just the board)
< div id = " flip-board "
</ div
< script type = " module "
import Board from '/flipoff/js/Board.js' ; import SoundEngine from '/flipoff/js/SoundEngine.js' ; import { MESSAGES , AUTO_ROTATE_INTERVAL } from '/flipoff/js/constants.js' ; const board = new Board ( document . getElementById ( 'flip-board' ) ) ; const sound = new SoundEngine ( ) ; let idx = 0 ; board . setMessage ( MESSAGES [ idx ] ) ; document . addEventListener ( 'click' , async ( ) => { await sound . init ( ) ; } , { once : true } ) ; setInterval ( ( ) => { idx = ( idx + 1 ) % MESSAGES . length ; board . setMessage ( MESSAGES [ idx ] ) ; sound . play ( ) ; } , AUTO_ROTATE_INTERVAL ) ; </ script
Custom Color Themes // js/constants.js — dark blue terminal theme export const SCRAMBLE_COLORS = [ '#0D1B2A' , '#1B2838' , '#00FF41' , '#003459' , '#007EA7' , '#00A8E8' ] ; export const ACCENT_COLORS = [ '#00FF41' , '#007EA7' , '#00A8E8' ] ; / css/board.css — override tile background / .tile { background-color :
0D1B2A
; color :
00FF41
; border-color :
003459
;
}
Common Patterns
Show real-time data (e.g., a flight API)
import
Board
from
'./js/Board.js'
;
import
SoundEngine
from
'./js/SoundEngine.js'
;
const
board
=
new
Board
(
document
.
getElementById
(
'board'
)
)
;
const
sound
=
new
SoundEngine
(
)
;
async
function
fetchAndDisplay
(
)
{
const
res
=
await
fetch
(
'/api/departures'
)
;
const
data
=
await
res
.
json
(
)
;
const
message
=
${
data
.
flight
}
${
data
.
destination
}
${
data
.
gate
}
;
board
.
setMessage
(
message
.
toUpperCase
(
)
)
;
sound
.
play
(
)
;
}
document
.
addEventListener
(
'click'
,
(
)
=>
sound
.
init
(
)
,
{
once
:
true
}
)
;
setInterval
(
fetchAndDisplay
,
30_000
)
;
fetchAndDisplay
(
)
;
Cycle through a custom message list
const
promos
=
[
"SALE ENDS SUNDAY"
,
"FREE SHIPPING OVER $50"
,
"NEW ARRIVALS THIS WEEK"
,
]
;
let
i
=
0
;
setInterval
(
(
)
=>
{
board
.
setMessage
(
promos
[
i
%
promos
.
length
]
)
;
sound
.
play
(
)
;
i
++
;
}
,
5000
)
;
React/Vue wrapper (import as a module)
// FlipBoard.jsx
import
{
useEffect
,
useRef
}
from
'react'
;
import
Board
from
'../flipoff/js/Board.js'
;
import
{
MESSAGES
}
from
'../flipoff/js/constants.js'
;
export
default
function
FlipBoard
(
{
messages
=
MESSAGES
,
interval
=
8000
}
)
{
const
containerRef
=
useRef
(
null
)
;
const
boardRef
=
useRef
(
null
)
;
useEffect
(
(
)
=>
{
boardRef
.
current
=
new
Board
(
containerRef
.
current
)
;
let
idx
=
0
;
boardRef
.
current
.
setMessage
(
messages
[
idx
]
)
;
const
timer
=
setInterval
(
(
)
=>
{
idx
=
(
idx
+
1
)
%
messages
.
length
;
boardRef
.
current
.
setMessage
(
messages
[
idx
]
)
;
}
,
interval
)
;
return
(
)
=>
clearInterval
(
timer
)
;
}
,
[
]
)
;
return
<
div
ref
=
{
containerRef
}
className
=
"
flip-board-container
"
/>
;
}
Troubleshooting
Problem
Fix
No sound
User must click/interact first; Web Audio requires a user gesture
Sound works locally but not deployed
Ensure
flapAudio.js
(base64) is served; check MIME types
Tiles don't animate
Verify CSS
tile.css
is loaded; check for JS console errors
Grid overflows on small screens
Reduce
GRID_COLS
/
GRID_ROWS
in
constants.js
or add CSS
overflow: hidden
Fullscreen not working
F
key calls
requestFullscreen()
— some browsers require the page to be focused
Messages cut off
String length exceeds
GRID_COLS × GRID_ROWS
; shorten or increase grid size
Audio blocked by CSP
Add
media-src 'self' blob: data:
to your Content-Security-Policy
CORS error loading modules
Serve with a local server (
python3 -m http.server
), not
file://
Tips for TV / Kiosk Deployment
Serve with a simple static server
npx serve .
Node
python3 -m http.server
Python
Auto-launch fullscreen in Chromium kiosk mode
chromium-browser --kiosk --app = http://localhost:8080
Hide cursor after idle (add to index.html)
document.addEventListener ( 'mousemove' , ( ) =
{ document.body.style.cursor = 'default' ; clearTimeout ( window._cursorTimer ) ; window._cursorTimer = setTimeout (( ) =
{ document.body.style.cursor = 'none' ; } , 3000 ) ; } ) ;