PySimpleGUI: Using an Image as a Button

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:
- PySimpleGUI 4.x (last release 4.60.5) was open source under the LGPL.
- PySimpleGUI 5, released in 2024, moved to a proprietary license that required a registration key, with paid licenses for commercial use.
- In February 2025 the maintainers announced that the company behind it would shut down, with support continuing to the end of 2025.
- In 2026, the author re-released the version 5 code, minus the licensing components, as PySimpleGUI 6 under the LGPLv3 on PyPI. The PyPI page is candid that the long-term outlook is uncertain.
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:
image_subsample=nshrinks the image by an integer factor:2shows every second pixel (half size),3one third, and so on. In our test a 48×48 PNG withimage_subsample=2produced a 26×26 widget (24 px of image plus the widget's padding).image_zoom=nenlarges by an integer factor. It exists in FreeSimpleGUI and PySimpleGUI 6, but not in every older 4.x release; check your version.image_size=(w, h)sets the button's size in pixels. It does not rescale the image; it crops or pads around it.
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:
- The event value is the button's
key. Image buttons have no meaningful entry invalues. update()acceptsimage_data,image_filename,image_subsampleandimage_size, so you can change scaling at the same time as the picture.- PySimpleGUI 6 also has a
mouseover_image_sourceparameter for a separate hover image. FreeSimpleGUI does not, so code that uses it is no longer portable between the two. - Disable a button with
window[key].update(disabled=True). The image stays visible, so consider swapping in a grayed-out version to make the state obvious. - Keep the decoded base64 values in module-level constants instead of re-reading files inside the event loop.
- For a whole toolbar, build the row with a list comprehension, giving each button a distinct key such as
("-TOOL-", name); tuple keys are allowed and make dispatch easy.
Troubleshooting
- An error popup saying "Button Element error" when the window opens: the library catches Tk's image error (typically "couldn't recognize data in image file") and shows it in a popup with a traceback instead of raising. The usual causes are a file that is not really PNG or GIF (for example a JPEG renamed to
.png) orimage_datareceiving a file path instead of image bytes. - Image does not appear but no error: check the working directory for relative paths, and make sure you passed
image_filenamerather than putting the path in the button text. - Visible frame around the image: set
border_width=0and matchbutton_colorto the container's background. On macOS, the native Tk button styling can limit how far this works. - Background flashes on hover or click: set
mouseover_colorsto the same background pair. - Image is too big: use
image_subsamplefor integer reduction or resize the source;image_sizealone will crop. ModuleNotFoundError: No module named 'tkinter': your Python build lacks Tk. On Debian/Ubuntu install thepython3-tkpackage for the matching Python version; the python.org installers for Windows and macOS include it.
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.
ToolsInstall Tkinter for PyCharm
Tkinter ships with Python and cannot be installed with pip. Check for it, add it on Windows, macOS or Linux, and point PyCharm at an interpreter that has it.
ToolsInstall packages in PyCharm
Install Python packages in PyCharm via the Packages tool window, interpreter settings or pip; venv vs conda, requirements.txt and fixing import errors.
Python'NoneType' object is not subscriptable
Fix 'NoneType' object is not subscriptable: common causes (list.sort(), dict.get, re.match, JSON null), reading the traceback, and catching it with mypy.