diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,9 @@
 # Revision history for reflex-vty
 
+## 1.0.0.1
+
+* Add some more examples, especially of the new canvas features
+
 ## 1.0.0.0
 
 * *Breaking change*: Add `HasColorProfile` class (with `colorProfile`/`localColorProfile` and a `ColorProfileReader` transformer) to `Reflex.Vty.Widget`. Widgets can call `colorProfile` to make decisions based on terminal capability.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -49,7 +49,23 @@
 
 Run the bundled demo with `cabal run example` to see many of these in action: a text editor, a to-do list, scrollable text, clickable buttons, a live CPU-usage display with a true-color gradient bar, a scrollbar-modes demo, a terminal-cursor demo, and a little styling showcase featuring gradients, color operations, a canvas overlay, border presets, theming, and color-profile downsampling.
 
-Feature requests, pull requests, and other feedback are welcome and appreciated (see the [contribution guide](CONTRIBUTING.md)). This library is still experimental, so big changes are possible.
+### Example gallery
+
+Each of these examples is a module in [`src-bin/Example/`](src-bin/Example/). You can run any of them with `cabal run examples -- <name>`.
+
+|                                                 |                                                                                                     |
+| :--                                             | :--                                                                                                 |
+| **`spinner`** — an animated braille spinner     | <img src="https://vhs.charm.sh/vhs-2ZldM9gFD9ovea54MAwio3.gif" alt="spinner demo" width="420">      |
+| **`progress`** — a gradient-filled progress bar | <img src="https://vhs.charm.sh/vhs-3oJNQQcQH6vM4VyH7Yww56.gif" alt="progress bar demo" width="420"> |
+| **`stopwatch`** — a tick-driven count-up timer  | <img src="https://vhs.charm.sh/vhs-5C8hkG3W9P4tATBBtLhxIm.gif" alt="stopwatch demo" width="420">    |
+| **`timer`** — a count-down timer                | <img src="https://vhs.charm.sh/vhs-3gpq3fy1zWP3IsXysnT7WT.gif" alt="timer demo" width="420">        |
+| **`textinput`** — a focused text-input field    | <img src="https://vhs.charm.sh/vhs-4FzSVaJAnGq4V1NNrk1lal.gif" alt="text input demo" width="420">   |
+| **`pager`** — a scrollable text viewport        | <img src="https://vhs.charm.sh/vhs-7a2bCmFHleF9MG3KxY4yeh.gif" alt="pager demo" width="420">        |
+| **`views`** — toggle between two views          | <img src="https://vhs.charm.sh/vhs-7rflGBcgFrmIQgk0JmZNKe.gif" alt="views demo" width="420">        |
+
+**More demos**: full-screen canvas animations (the classic Doom fire, a plasma field, a starfield, an animated splash) live on the [examples page](EXAMPLES.md).
+
+Feature requests, pull requests, and other feedback are welcome and appreciated (see the [contribution guide](CONTRIBUTING.md)).
 
 ### How to Build
 
diff --git a/reflex-vty.cabal b/reflex-vty.cabal
--- a/reflex-vty.cabal
+++ b/reflex-vty.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: reflex-vty
-version: 1.0.0.0
+version: 1.0.0.1
 synopsis: Reflex FRP host and widgets for VTY applications
 description:
   Build terminal applications using functional reactive programming (FRP) with Reflex FRP (<https://reflex-frp.org>).
@@ -151,6 +151,40 @@
     TupleSections
     TypeApplications
     TypeFamilies
+
+executable examples
+  hs-source-dirs: src-bin
+  main-is: examples.hs
+  ghc-options: -threaded -Wall
+  build-depends:
+    base,
+    containers,
+    reflex,
+    reflex-vty,
+    text,
+    time,
+    vty
+  default-language: Haskell2010
+  other-modules:
+    Example.CellBuffer
+    Example.Common
+    Example.DoomFire
+    Example.Pager
+    Example.Progress
+    Example.Space
+    Example.Spinner
+    Example.Splash
+    Example.Stopwatch
+    Example.TextInput
+    Example.Timer
+    Example.Views
+  default-extensions:
+    ConstraintKinds
+    FlexibleContexts
+    LambdaCase
+    OverloadedStrings
+    RankNTypes
+    ScopedTypeVariables
 
 executable leaktest
   if !flag(leaktest)
diff --git a/src-bin/Example/CellBuffer.hs b/src-bin/Example/CellBuffer.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/CellBuffer.hs
@@ -0,0 +1,48 @@
+-- |
+--   Description: An animated plasma field rendered cell-by-cell, à la the
+--   cellbuffer example.
+module Example.CellBuffer (cellBuffer) where
+
+import Control.Monad.IO.Class (liftIO)
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+-- | A classic plasma effect: each cell's character and color are a pure
+-- function of its position and the frame, summed over a few sine waves.
+cellBuffer :: Demo t m => m (Event t ())
+cellBuffer = do
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.06 t0
+  n <- foldDyn (\_ i -> i + 1) (0 :: Int) tick
+  dw <- displayWidth
+  dh <- displayHeight
+  tellImages $ (\w h i -> [plasma w h i]) <$> current dw <*> current dh <*> current n
+  quit
+  where
+    ramp = " .:-=+*#%@"
+    grad =
+      gradient1D
+        [ (0, RGB 25 20 70)
+        , (0.4, RGB 190 40 140)
+        , (0.7, RGB 255 150 40)
+        , (1, RGB 255 240 150)
+        ]
+    plasma w h i =
+      let t = fromIntegral i * 0.12 :: Double
+          cell x y =
+            let fx = fromIntegral x
+                fy = fromIntegral y
+                v =
+                  sin (fx * 0.18 + t)
+                    + sin (fy * 0.27 + t * 1.2)
+                    + sin ((fx + fy) * 0.12 + t * 0.8)
+                    + sin (sqrt (fx * fx + fy * fy) * 0.2 - t)
+                u = (v + 4) / 8 -- normalize roughly to [0,1]
+                ci = max 0 $ min (length ramp - 1) $ floor (u * fromIntegral (length ramp))
+                clr = fromRGB $ sampleGradient1D grad u
+            in V.char (V.withForeColor V.defAttr clr) (ramp !! ci)
+      in V.vertCat [V.horizCat [cell x y | x <- [0 .. w - 1]] | y <- [0 .. h - 1]]
diff --git a/src-bin/Example/Common.hs b/src-bin/Example/Common.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Common.hs
@@ -0,0 +1,44 @@
+-- |
+--   Description: Shared scaffolding for the small standalone example demos.
+module Example.Common where
+
+import Control.Monad.Fix (MonadFix)
+import Control.Monad.IO.Class (MonadIO)
+import qualified Graphics.Vty as V
+import Reflex
+
+import Reflex.Vty
+
+-- | The set of capabilities the little single-screen demos rely on. They draw
+-- with 'tellImages'\/'text', read the clock via 'tickLossy', and quit on a key.
+type Demo t m =
+  ( Reflex t
+  , MonadFix m
+  , MonadHold t m
+  , MonadIO m
+  , MonadIO (Performable m)
+  , PerformEvent t m
+  , PostBuild t m
+  , TriggerEvent t m
+  , HasInput t m
+  , HasImageWriter t m
+  , HasDisplayRegion t m
+  , HasFocusReader t m
+  , HasTheme t m
+  )
+
+-- | The brand accent color (cyan), matching the rest of the demos.
+accent :: V.Color
+accent = rgbColor 0 200 230
+
+-- | A dim grey for hints.
+dim :: V.Color
+dim = rgbColor 120 120 140
+
+-- | An 'Event' that fires when the user asks to quit (q or Ctrl+C). Demos
+-- return this from their widget so 'mainWidget' shuts down.
+quit :: (Reflex t, Monad m, HasInput t m) => m (Event t ())
+quit = do
+  q <- key (V.KChar 'q')
+  c <- ctrlc
+  pure $ leftmost [() <$ q, c]
diff --git a/src-bin/Example/DoomFire.hs b/src-bin/Example/DoomFire.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/DoomFire.hs
@@ -0,0 +1,63 @@
+-- |
+--   Description: The classic Doom fire effect, à la the doom-fire example.
+module Example.DoomFire (doomFire) where
+
+import Control.Monad.IO.Class (liftIO)
+import Data.Bits (shiftR, xor)
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+maxHeat :: Int
+maxHeat = 36
+
+-- | A heat grid seeded from a hot bottom row; each frame every cell cools and
+-- drifts upward from the cell below it, producing rising flames. The grid is
+-- sized to the terminal when the demo starts.
+doomFire :: Demo t m => m (Event t ())
+doomFire = do
+  w <- sample . current =<< displayWidth
+  h <- sample . current =<< displayHeight
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.05 t0
+  st <- foldDyn (\_ (g, f) -> (step w h f g, f + 1)) (initGrid w h, 0 :: Int) tick
+  tellImages $ current $ ffor st $ \(g, _) -> [renderFire g]
+  quit
+  where
+    initGrid w h =
+      [[if y == h - 1 then maxHeat else 0 | _ <- [0 .. w - 1]] | y <- [0 .. h - 1]]
+    step w h f grid =
+      let stepRow y below =
+            [ let r = rnd x y f
+                  srcX = max 0 $ min (w - 1) $ x + ((r `div` 5) `mod` 3 - 1)
+                  decay = r `mod` 5
+              in max 0 (below !! srcX - decay)
+            | x <- [0 .. w - 1]
+            ]
+      in [stepRow y (grid !! (y + 1)) | y <- [0 .. h - 2]] ++ [replicate w maxHeat]
+    renderFire grid = V.vertCat [V.horizCat (map cell rowHeats) | rowHeats <- grid]
+    cell heat
+      | heat <= 0 = V.char V.defAttr ' '
+      | otherwise = V.char (V.withForeColor V.defAttr (heatColor heat)) (heatChar heat)
+    heatChar heat
+      | heat < 10 = '░'
+      | heat < 19 = '▒'
+      | heat < 28 = '▓'
+      | otherwise = '█'
+    heatColor heat = fromRGB $ sampleGradient1D fireGrad (fromIntegral heat / fromIntegral maxHeat)
+    fireGrad =
+      gradient1D
+        [ (0.0, RGB 12 8 8)
+        , (0.18, RGB 90 0 0)
+        , (0.4, RGB 210 35 0)
+        , (0.6, RGB 255 110 0)
+        , (0.8, RGB 255 200 40)
+        , (1.0, RGB 255 255 200)
+        ]
+    rnd x y f =
+      let a = x * 374761393 + y * 668265263 + f * 2246822519 + 12345
+          b = (a `xor` (a `shiftR` 13)) * 1274126177
+      in abs (b `xor` (b `shiftR` 16))
diff --git a/src-bin/Example/Pager.hs b/src-bin/Example/Pager.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Pager.hs
@@ -0,0 +1,28 @@
+-- |
+--   Description: A scrollable pager, à la bubbles\/viewport (pager example).
+module Example.Pager (pager) where
+
+import qualified Data.Text as T
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+-- | A 'scrollableText' viewport over a block of numbered lines. Scroll it with
+-- the arrow keys or the mouse wheel.
+pager :: Demo t m => m (Event t ())
+pager = do
+  tellImages $
+    pure [V.text' (V.withForeColor V.defAttr accent) "  ↑/↓ or mouse wheel to scroll · q to quit"]
+  dw <- displayWidth
+  dh <- displayHeight
+  let viewport w h = Region 0 2 w (max 1 (h - 2))
+  _ <- pane (viewport <$> dw <*> dh) (pure True) $ scrollableText def (pure body)
+  quit
+  where
+    body =
+      T.unlines
+        [ "Line " <> T.pack (show n) <> " — scrollable viewport content"
+        | n <- [1 .. 60 :: Int]
+        ]
diff --git a/src-bin/Example/Progress.hs b/src-bin/Example/Progress.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Progress.hs
@@ -0,0 +1,36 @@
+-- |
+--   Description: An animated, gradient-filled progress bar, à la bubbles\/progress.
+module Example.Progress (progressBar) where
+
+import Control.Monad.IO.Class (liftIO)
+import qualified Data.Text as T
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+import Text.Printf (printf)
+
+import Example.Common
+import Reflex.Vty
+
+-- | A progress bar that fills 0→100% (then loops), tinted with a red→yellow→
+-- green 'Gradient1D' so each cell carries its own color.
+progressBar :: Demo t m => m (Event t ())
+progressBar = do
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.04 t0
+  pct <- foldDyn (\_ p -> if p >= 1 then 0 else min 1 (p + 0.008)) (0 :: Double) tick
+  dw <- displayWidth
+  tellImages $ (\w p -> [barImage w p]) <$> current dw <*> current pct
+  quit
+  where
+    grad = gradient1D [(0, RGB 255 70 90), (0.5, RGB 250 200 60), (1, RGB 80 220 120)]
+    barImage w p =
+      let barW = max 1 (w - 7)
+          filled = round (p * fromIntegral barW) :: Int
+          cellColor i = fromRGB $ sampleGradient1D grad (fromIntegral i / fromIntegral (max 1 (barW - 1)))
+          cell i
+            | i < filled = V.char (V.withForeColor V.defAttr (cellColor i)) '█'
+            | otherwise = V.char (V.withForeColor V.defAttr (rgbColor 64 64 78)) '░'
+          bar = V.horizCat $ map cell [0 .. barW - 1]
+          label = V.text' (V.withForeColor V.defAttr accent) $ T.pack $ printf " %3d%%" (round (p * 100) :: Int)
+      in V.horizCat [bar, label]
diff --git a/src-bin/Example/Space.hs b/src-bin/Example/Space.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Space.hs
@@ -0,0 +1,50 @@
+-- |
+--   Description: A drifting starfield, à la the space example.
+module Example.Space (space) where
+
+import Control.Monad.IO.Class (liftIO)
+import Data.Bits (shiftR, xor)
+import qualified Data.Map as Map
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+-- | A starfield warping leftward. Each star has a pseudo-random row and speed
+-- (its "depth"); faster stars are brighter and drawn with a bolder glyph.
+space :: Demo t m => m (Event t ())
+space = do
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.06 t0
+  n <- foldDyn (\_ i -> i + 1) (0 :: Int) tick
+  dw <- displayWidth
+  dh <- displayHeight
+  tellImages $ (\w h i -> [starfield w h i]) <$> current dw <*> current dh <*> current n
+  quit
+  where
+    starfield w h i
+      | w <= 0 || h <= 0 = V.emptyImage
+      | otherwise =
+          let nStars = max 1 (w * h `div` 14)
+              stars = Map.fromList [star w h i k | k <- [0 .. nStars - 1]]
+              mkRow y = V.horizCat [Map.findWithDefault gap (x, y) stars | x <- [0 .. w - 1]]
+          in V.vertCat [mkRow y | y <- [0 .. h - 1]]
+    gap = V.char V.defAttr ' '
+    star w h i k =
+      let y = hash (k * 2 + 1) `mod` h
+          speed = 1 + hash (k * 3 + 2) `mod` 3
+          x = (hash k - i * speed) `mod` w
+          (ch, clr) = case speed of
+            1 -> ('·', rgbColor 90 90 120)
+            2 -> ('*', rgbColor 170 170 205)
+            _ -> ('✦', rgbColor 255 255 255)
+      in ((x, y), V.char (V.withForeColor V.defAttr clr) ch)
+    -- A cheap integer hash, always non-negative.
+    hash :: Int -> Int
+    hash k0 =
+      let a = k0 * 374761393 + 668265263
+          b = (a `xor` (a `shiftR` 13)) * 1274126177
+          c = b `xor` (b `shiftR` 16)
+      in abs c
diff --git a/src-bin/Example/Spinner.hs b/src-bin/Example/Spinner.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Spinner.hs
@@ -0,0 +1,29 @@
+-- |
+--   Description: An animated spinner, à la bubbles\/spinner.
+module Example.Spinner (spinner) where
+
+import Control.Monad.IO.Class (liftIO)
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+-- | A braille spinner that cycles a frame every 80ms.
+spinner :: Demo t m => m (Event t ())
+spinner = do
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.08 t0
+  idx <- foldDyn (\_ i -> (i + 1) `mod` n) 0 tick
+  tellImages $ current $ ffor idx frame
+  quit
+  where
+    frames = "⣾⣽⣻⢿⡿⣟⣯⣷"
+    n = length frames
+    frame i =
+      [ V.horizCat
+          [ V.char (V.withForeColor V.defAttr accent) (frames !! i)
+          , V.text' (V.withForeColor V.defAttr dim) "  Spinning forever…  (q to quit)"
+          ]
+      ]
diff --git a/src-bin/Example/Splash.hs b/src-bin/Example/Splash.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Splash.hs
@@ -0,0 +1,51 @@
+-- |
+--   Description: An animated splash screen, à la the splash example.
+module Example.Splash (splash) where
+
+import Control.Monad.IO.Class (liftIO)
+import qualified Data.Text as T
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+-- | A centered title that types out in a pink→violet→cyan gradient, with a
+-- subtitle that fades in beneath it; the whole thing loops.
+splash :: Demo t m => m (Event t ())
+splash = do
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.06 t0
+  n <- foldDyn (\_ i -> i + 1) (0 :: Int) tick
+  dw <- displayWidth
+  dh <- displayHeight
+  tellImages $ (\w h i -> [frame w h i]) <$> current dw <*> current dh <*> current n
+  quit
+  where
+    title = "reflex-vty"
+    subtitle = "reactive terminal UIs"
+    grad = gradient1D [(0, RGB 255 64 160), (0.5, RGB 150 90 255), (1, RGB 0 200 230)]
+    frame w h i =
+      let p = i `mod` 95
+          revealed = min (length title) (max 0 (p `div` 2))
+          subAlpha = max 0 (min 1 (fromIntegral (p - 26) / 18 :: Double))
+          pad k = V.text' V.defAttr (T.replicate (max 0 k) " ")
+          center len = (w - len) `div` 2
+          titleImg =
+            V.horizCat
+              [ V.char (V.withStyle (V.withForeColor V.defAttr (charColor k)) V.bold) c
+              | (k, c) <- zip [0 :: Int ..] (take revealed title)
+              ]
+          charColor k = fromRGB $ sampleGradient1D grad (fromIntegral k / fromIntegral (max 1 (length title - 1)))
+          subImg
+            | subAlpha <= 0 = V.emptyImage
+            | otherwise = V.text' (V.withForeColor V.defAttr (fromRGB (mix subAlpha (RGB 35 35 45) (RGB 150 150 170)))) (T.pack subtitle)
+          gap = V.char V.defAttr ' '
+          top = max 0 (h `div` 2 - 1)
+      in V.vertCat $
+           replicate top gap
+             ++ [ V.horizCat [pad (center (length title)), titleImg]
+                , gap
+                , V.horizCat [pad (center (length subtitle)), subImg]
+                ]
diff --git a/src-bin/Example/Stopwatch.hs b/src-bin/Example/Stopwatch.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Stopwatch.hs
@@ -0,0 +1,30 @@
+-- |
+--   Description: A count-up stopwatch, à la bubbles\/stopwatch.
+module Example.Stopwatch (stopwatch) where
+
+import Control.Monad.IO.Class (liftIO)
+import qualified Data.Text as T
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+import Text.Printf (printf)
+
+import Example.Common
+import Reflex.Vty
+
+-- | Counts up from zero, ticking every 100ms.
+stopwatch :: Demo t m => m (Event t ())
+stopwatch = do
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.1 t0
+  elapsed <- foldDyn (\_ d -> d + 0.1) (0 :: Double) tick
+  tellImages $ current $ ffor elapsed line
+  quit
+  where
+    line d =
+      [ V.horizCat
+          [ V.text' (V.withForeColor V.defAttr accent) "  ⏱  "
+          , V.text' V.defAttr $ T.pack $ printf "%.1fs" d
+          , V.text' (V.withForeColor V.defAttr dim) "   (q to quit)"
+          ]
+      ]
diff --git a/src-bin/Example/TextInput.hs b/src-bin/Example/TextInput.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/TextInput.hs
@@ -0,0 +1,27 @@
+-- |
+--   Description: A single text input field, à la bubbles\/textinput.
+module Example.TextInput (textInputExample) where
+
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+-- | A prompt and a 'textInput' below it. The input lives in an always-focused
+-- 'pane', so it accepts typing immediately. Quit with Ctrl+C (q is a character).
+textInputExample :: Demo t m => m (Event t ())
+textInputExample = do
+  dw <- displayWidth
+  tellImages $
+    pure
+      [ V.vertCat
+          [ V.text' (V.withForeColor V.defAttr accent) "  What's your name?"
+          , V.text' V.defAttr ""
+          ]
+      ]
+  let inputRegion w = Region 2 2 (max 1 (w - 4)) 1
+  _ <- pane (inputRegion <$> dw) (pure True) $ textInput def
+  tellImages $
+    pure [V.translate 2 4 $ V.text' (V.withForeColor V.defAttr dim) "(type away — Ctrl+C to quit)"]
+  ctrlc
diff --git a/src-bin/Example/Timer.hs b/src-bin/Example/Timer.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Timer.hs
@@ -0,0 +1,31 @@
+-- |
+--   Description: A countdown timer, à la bubbles\/timer.
+module Example.Timer (timer) where
+
+import Control.Monad.IO.Class (liftIO)
+import qualified Data.Text as T
+import Data.Time (getCurrentTime)
+import qualified Graphics.Vty as V
+import Reflex
+import Text.Printf (printf)
+
+import Example.Common
+import Reflex.Vty
+
+-- | Counts down from ten seconds, ticking every 100ms.
+timer :: Demo t m => m (Event t ())
+timer = do
+  t0 <- liftIO getCurrentTime
+  tick <- tickLossy 0.1 t0
+  remaining <- foldDyn (\_ r -> max 0 (r - 0.1)) (10 :: Double) tick
+  tellImages $ current $ ffor remaining line
+  quit
+  where
+    line r =
+      [ V.horizCat
+          [ V.text' (V.withForeColor V.defAttr accent) "  ⏳  "
+          , V.text' V.defAttr $ T.pack $ printf "%.1fs" r
+          , V.text' (V.withForeColor V.defAttr dim) $
+              if r <= 0 then "   time's up!  (q to quit)" else "   remaining  (q to quit)"
+          ]
+      ]
diff --git a/src-bin/Example/Views.hs b/src-bin/Example/Views.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/Example/Views.hs
@@ -0,0 +1,36 @@
+-- |
+--   Description: Switching between views, à la the composable-views example.
+module Example.Views (views) where
+
+import qualified Graphics.Vty as V
+import Reflex
+
+import Example.Common
+import Reflex.Vty
+
+-- | Two views toggled with Tab. Each is just a different rendering chosen from
+-- a 'Dynamic' index — the FRP way to swap "views".
+views :: Demo t m => m (Event t ())
+views = do
+  tab <- key (V.KChar '\t')
+  n <- foldDyn (\_ i -> (i + 1) `mod` 2) (0 :: Int) tab
+  tellImages $ current $ ffor n viewImage
+  quit
+  where
+    hint = V.text' (V.withForeColor V.defAttr dim) "  Tab to switch views · q to quit"
+    viewImage 0 =
+      [ V.vertCat
+          [ V.text' (V.withStyle (V.withForeColor V.defAttr accent) V.bold) "  ◖ View one ◗"
+          , V.text' V.defAttr "  reactive, composable terminal UIs"
+          , V.char V.defAttr ' '
+          , hint
+          ]
+      ]
+    viewImage _ =
+      [ V.vertCat
+          [ V.text' (V.withStyle (V.withForeColor V.defAttr (rgbColor 200 120 255)) V.bold) "  ◆ View two ◆"
+          , V.text' V.defAttr "  swapped with a single keystroke"
+          , V.char V.defAttr ' '
+          , hint
+          ]
+      ]
diff --git a/src-bin/examples.hs b/src-bin/examples.hs
new file mode 100644
--- /dev/null
+++ b/src-bin/examples.hs
@@ -0,0 +1,50 @@
+-- | A gallery of small, standalone reflex-vty demos — ports of the Bubble Tea
+-- examples (https://github.com/charmbracelet/bubbletea/tree/main/examples).
+--
+-- Run one with: @cabal run examples -- <name>@ (defaults to @spinner@).
+module Main where
+
+import qualified Data.Text as T
+import Reflex
+import System.Environment (getArgs)
+
+import Example.CellBuffer
+import Example.Common
+import Example.DoomFire
+import Example.Pager
+import Example.Progress
+import Example.Space
+import Example.Spinner
+import Example.Splash
+import Example.Stopwatch
+import Example.TextInput
+import Example.Timer
+import Example.Views
+import Reflex.Vty
+
+main :: IO ()
+main = do
+  args <- getArgs
+  let name = case args of
+        (a : _) -> a
+        _ -> "spinner"
+  mainWidget def $ runExample name
+
+-- | Dispatch to a demo by name.
+runExample :: Demo t m => String -> m (Event t ())
+runExample = \case
+  "spinner" -> spinner
+  "progress" -> progressBar
+  "progress-bar" -> progressBar
+  "stopwatch" -> stopwatch
+  "timer" -> timer
+  "textinput" -> textInputExample
+  "pager" -> pager
+  "views" -> views
+  "cellbuffer" -> cellBuffer
+  "doom-fire" -> doomFire
+  "space" -> space
+  "splash" -> splash
+  other -> do
+    text $ pure $ "Unknown example: " <> T.pack other <> "  (q to quit)"
+    quit
