2026, Jan 01 01:00

Build a Clean Tkinter Teleprompter: Static Headline, Scrolling Text, and a Cropped-Background Canvas

Build a Tkinter teleprompter overlay: keep a fullscreen background visible, pin the headline, and scroll long text using a nested Canvas viewport to clip edges.

Building a teleprompter-like overlay in Tkinter seems straightforward until text starts to scroll over a fullscreen background. The background must stay visible, the headline should remain static, and only the long body of text should move. In a single Canvas, though, this quickly turns into a layering and clipping headache.

Problem setup

The window is fullscreen with a Canvas that first renders a background image. A headline is placed in the top-right corner, and a long text block is added below it. The text is then animated upward using Canvas.move(), mimicking a teleprompter.

import tkinter as tk
from PIL import Image, ImageTk

root = tk.Tk()
stage = tk.Canvas(root, bg="white", bd=0)
stage.pack(fill=tk.BOTH, expand=True)
stage.update()

img_src = Image.open('bild.jpg')
resized_img = img_src.resize((stage.winfo_width(), stage.winfo_height()), Image.LANCZOS)
bg_photo = ImageTk.PhotoImage(resized_img, master=stage)
stage.create_image(0, 0, anchor="nw", image=bg_photo)
stage.update()

panel_w = int(stage.winfo_width() * 0.45)
panel_x = stage.winfo_width() - panel_w

title_id = stage.create_text(
    panel_x + 10, 10,
    anchor='nw',
    text=headline,
    font=('Helvetica', 18, 'bold'),
    fill='black',
    width=panel_w - 20
)
a, b, c, d = stage.bbox(title_id)

scroll_id = stage.create_text(
    panel_x + 10, d + 10,
    anchor='nw',
    text=fulltext,
    font=('Helvetica', 14, 'normal'),
    fill='black',
    width=panel_w - 20
)
stage.update()

def scroll_loop():
    stage.move(scroll_id, 0, -1)
    x1, y1, x2, y2 = stage.bbox(scroll_id)
    if y2 > stage.winfo_height():
        root.after(60, scroll_loop)

The symptom: while the long text scrolls up, it visually interferes with the headline. The background must stay visible at all times, but the Canvas stacking alone can’t deliver the right composition, and trying to split elements across different widgets makes transparency disappear.

Why it behaves this way

Text drawn on a Canvas doesn’t carry a background; it’s transparent by default. That means every element underneath is always visible, and ordering alone won’t produce a clean teleprompter window where only a defined region scrolls over a fixed background. At the same time, tkinter.Canvas does not support drawing text in clipped area. You don’t get a native viewport that clips the text as it moves, so the long text will overlap with other items in ways that are hard to control if everything lives on the same surface.

To make a portion of the interface look transparent while actually being independent and scrollable, something must sit between the text and what’s behind it, and that “something” must visually match the background where it’s placed. That’s where an embedded Canvas with a cropped background image comes in.

Fix: a nested Canvas with a cropped background

The idea is to mount a second Canvas as the teleprompter surface. Crop the background image to the exact position and size of that surface and draw it inside the child Canvas. This makes the child look transparent over the background, but it gives you an isolated area to scroll text and naturally clip it at the edges.

import tkinter as tk
from PIL import Image, ImageTk, ImageGrab

title_text = 'Headline'
with open(__file__) as f:
    body_text = f.read()

root = tk.Tk()
root.geometry('800x600')

stage = tk.Canvas(root, bg="white", bd=0)
stage.pack(fill=tk.BOTH, expand=True)
stage.update()

img_src = Image.open('lena.jpg')
resized_img = img_src.resize((stage.winfo_width(), stage.winfo_height()), Image.LANCZOS)
bg_photo = ImageTk.PhotoImage(resized_img, master=stage)
stage.create_image(0, 0, anchor="nw", image=bg_photo)

panel_w = int(stage.winfo_width() * 0.45)
panel_x = stage.winfo_width() - panel_w

title_id = stage.create_text(
    panel_x + 10, 10,
    anchor='nw',
    text=title_text,
    font=('Helvetica', 18, 'bold'),
    fill='black',
    width=panel_w - 20
)
x1, y1, x2, y2 = stage.bbox(title_id)

panel_h = stage.winfo_height() - y2 - 10

scroller = tk.Canvas(stage, width=panel_w - 20, height=panel_h - 10, highlightthickness=0)
stage.create_window(panel_x + 10, y2 + 10, window=scroller, anchor='nw')

crop_img = resized_img.crop((panel_x + 10, y2 + 10, resized_img.width, resized_img.height))
pane_bg = ImageTk.PhotoImage(crop_img)
scroller.create_image(0, 0, image=pane_bg, anchor='nw')

scroll_id = scroller.create_text(
    0, 0,
    anchor='nw',
    text=body_text,
    font=('Helvetica', 14, 'normal'),
    fill='black',
    width=panel_w
)

def scroll_loop():
    scroller.move(scroll_id, 0, -1)
    a, b, c, d = scroller.bbox(scroll_id)
    if d > scroller.winfo_height():
        root.after(50, scroll_loop)

scroll_loop()
root.mainloop()

The embedded Canvas serves as a viewport. Its background is a precise crop of the main background image, so it visually aligns. The long text is rendered inside and moved upward; the child Canvas edges clip it naturally. The static headline sits on the parent Canvas and remains unaffected.

Why this matters

Understanding what a Canvas can and can’t do saves time when composing layered UIs. When you need an element to appear transparent while scrolling over a background, you either introduce a matching visual layer underneath or you isolate the moving content into its own surface. Since tkinter.Canvas does not support drawing text in clipped area, creating a dedicated teleprompter Canvas with a background crop is a robust way to get a clean scroll without artifacts.

Takeaways

For teleprompter-style layouts with a static background and mixed static and moving text, a child Canvas with a cropped background is a pragmatic pattern. It keeps the background always visible, preserves a static headline, and confines the scroll to a predictable region. Keep the background rendering on the parent surface, mount a correctly positioned child Canvas for the scrolling area, and drive motion with move() and after() for smooth updates.