Python

PySimpleGUI: Using an Image as a Button

Python · Published 2026-10-24

A heap of colourful plastic sewing buttons seen from above
A heap of sewing buttons in every colour, seen from above.

Pass an image to sg.Button with image_filename="icon.png" or image_data=<base64 PNG>, then set border_width=0 and button_color=(bg, bg), where bg = sg.theme_background_color(), so only the image is visible. Clicks arrive as normal events named by the button's key, and window[key].update(image_data=...) swaps the image at runtime.

A note on PySimpleGUI versions and licensing

Before choosing a package, be aware of the project's turbulent recent history:

During the proprietary period, the community created FreeSimpleGUI, a fork of the last LGPL 4.x code base. It keeps the same API and is maintained independently, so migrating is usually a one-line change:

pip install FreeSimpleGUI          # community fork of 4.x (LGPL)
pip install PySimpleGUI            # upstream, LGPL again since version 6
import FreeSimpleGUI as sg      # or: import PySimpleGUI as sg

FreeSimpleGUI's own releases are numbered 5.x, which has nothing to do with the proprietary PySimpleGUI 5. Check the license of the exact version you install, especially if you pinned a 5.x PySimpleGUI release in an older project. All code below uses the tkinter port. We ran it with FreeSimpleGUI 5.1 and PySimpleGUI 6.3 on Python 3.12, and the image options it uses are unchanged since PySimpleGUI 4.60.

Option 1: image_filename

The simplest approach points the button at an image file on disk:

import FreeSimpleGUI as sg

layout = [
    [sg.Button(image_filename="play.png", key="-PLAY-")],
    [sg.Button("Exit")],
]
window = sg.Window("Player", layout)

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, "Exit"):
        break
    if event == "-PLAY-":
        print("Play clicked")

window.close()

With the tkinter port, use PNG or GIF files. Those are the formats Tk 8.6 can load natively. JPEG and other formats need to be converted first, for example with Pillow. Relative paths are resolved against the current working directory, not the script's folder, so build absolute paths with pathlib.Path(__file__).parent / "play.png" if the program may be started from elsewhere.

When a button has an image and no text, give it a key. Otherwise the event name is the (empty) button text, which makes events hard to tell apart.

Option 2: image_data with a base64 PNG

image_data accepts the image as base64-encoded bytes. This is the preferred method for distributable tools: icons live inside the script, so there are no paths to break and nothing extra to bundle with PyInstaller. Encode a file once:

import base64
from pathlib import Path

encoded = base64.b64encode(Path("play.png").read_bytes())
print(encoded[:40])   # paste the full value into your source as PLAY_PNG = b"..."

Then use it directly:

PLAY_PNG = b"iVBORw0KGgoAAAANSUhEUgAA..."   # truncated for display
sg.Button(image_data=PLAY_PNG, key="-PLAY-")

Raw PNG bytes (not base64) also work with the tkinter port in practice, but base64 is the documented, portable form. If you need a test image without any graphics tools, the standard library can generate a solid-color PNG:

import struct
import zlib
from pathlib import Path

def solid_png(width, height, rgba):
    """PNG bytes for a solid RGBA square, standard library only."""
    def chunk(kind, data):
        body = kind + data
        return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body))
    raw = (b"\x00" + bytes(rgba) * width) * height       # filter byte + pixels per row
    header = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
    return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", header)
            + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))

Path("on.png").write_bytes(solid_png(32, 32, (46, 160, 67, 255)))
Path("off.png").write_bytes(solid_png(32, 32, (200, 60, 60, 255)))

Making the button look like just an image

By default the image sits on a colored button with a raised border. To make it blend into the window, match the button color to the theme background and remove the border:

sg.theme("DarkGrey13")
bg = sg.theme_background_color()

sg.Button(
    image_data=PLAY_PNG,
    key="-PLAY-",
    button_color=(bg, bg),       # (text color, background color)
    border_width=0,
    mouseover_colors=(bg, bg),   # prevent a color flash on hover
)

Call sg.theme(...) before reading theme_background_color() and before creating the layout, otherwise you match the wrong color. If the button lives inside a Column or Frame with its own background_color, use that color instead.

Two notes on "transparency". First, tkinter buttons are not truly transparent; the trick works by painting the button the same color as what is behind it. Transparent pixels in a PNG show the button's background color, which is why matching it matters. Second, the library exposes a constant called sg.TRANSPARENT_BUTTON, but it is simply a fixed gray color pair, which only looks transparent on the old default theme. Matching the theme background is more reliable.

Controlling size: image_subsample and image_size

Tk cannot resample images smoothly, so size options are limited:

For anything other than integer scaling, resize the image beforehand (for example with Pillow's Image.resize and the LANCZOS filter) and pass the result as base64 data. Producing icons at the exact size you need gives the sharpest result.

Toggling images and handling events

A common need is a button whose picture reflects state: play/pause, mute/unmute, on/off. Store the state in the element's metadata attribute and swap the image with update():

import base64
from pathlib import Path

import FreeSimpleGUI as sg   # or: import PySimpleGUI as sg  (same API in 4.x)

ON_PNG = base64.b64encode(Path("on.png").read_bytes())
OFF_PNG = base64.b64encode(Path("off.png").read_bytes())

sg.theme("DarkGrey13")
bg = sg.theme_background_color()

layout = [
    [sg.Text("Click the image to toggle")],
    [
        sg.Button(
            image_data=OFF_PNG,
            key="-TOGGLE-",
            button_color=(bg, bg),
            border_width=0,
            mouseover_colors=(bg, bg),
            metadata=False,             # current state lives on the element
            tooltip="Toggle",
        ),
        sg.Button(image_filename="green.png", image_subsample=2, key="-HALF-",
                  button_color=(bg, bg), border_width=0),
    ],
    [sg.Text("State: off", key="-STATUS-", size=(20, 1))],
    [sg.Button("Exit")],
]

window = sg.Window("Image buttons", layout, finalize=True)

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, "Exit"):
        break
    if event == "-TOGGLE-":
        btn = window["-TOGGLE-"]
        btn.metadata = not btn.metadata
        btn.update(image_data=ON_PNG if btn.metadata else OFF_PNG)
        window["-STATUS-"].update(f"State: {'on' if btn.metadata else 'off'}")
    elif event == "-HALF-":
        sg.popup_quick_message("Half-size image clicked", background_color="black")

window.close()

We verified this layout under a virtual display by triggering clicks programmatically with window["-TOGGLE-"].click(): three clicks produced the events -TOGGLE- each time and the status cycled on, off, on.

A few details about events:

Troubleshooting

Summary

An image button is an ordinary sg.Button with image_filename or, better for distribution, base64 image_data. Make it look borderless with border_width=0 and button_color and mouseover_colors set to the theme or container background, scale with image_subsample, and swap images through update() while keeping state in metadata. Because of PySimpleGUI's licensing changes, check which package and version you depend on: the LGPL-licensed FreeSimpleGUI fork and the re-opened PySimpleGUI 6 both run this code unchanged. The element options are described in the FreeSimpleGUI documentation, and the underlying image support in the tkinter documentation.