A download button with custom CSS

[Update 2020-07-18] Revised the function to display a download button instead of a link after clicking a button, saving a UI step. The associated gist has been updated with the new version. Also, updated post title to reflect changes (hope that’s ok mods).

Hi, if it helps others, I created a first pass general file downloader for my personal Streamlit projects. I’ve also built an examples app to test it out with your files and to provide code snippets you can copy & paste.

See the following GitHub gist or code below.

I haven’t tested all files, and it’s an admittedly hacky solution I coded over the weekend. Improvements are welcome.

Run the example app with:

streamlit run https://gist.githubusercontent.com/chad-m/6be98ed6cf1c4f17d09b7f6e5ca2978f/raw/7ef3bf59741de2262a850020ab58e68947d3f052/streamlit_download_button.py

The download function code:


import base64
import os
import json
import pickle
import uuid
import re

import streamlit as st
import pandas as pd


def download_button(object_to_download, download_filename, button_text, pickle_it=False):
    """
    Generates a link to download the given object_to_download.

    Params:
    ------
    object_to_download:  The object to be downloaded.
    download_filename (str): filename and extension of file. e.g. mydata.csv,
    some_txt_output.txt download_link_text (str): Text to display for download
    link.
    button_text (str): Text to display on download button (e.g. 'click here to download file')
    pickle_it (bool): If True, pickle file.

    Returns:
    -------
    (str): the anchor tag to download object_to_download

    Examples:
    --------
    download_link(your_df, 'YOUR_DF.csv', 'Click to download data!')
    download_link(your_str, 'YOUR_STRING.txt', 'Click to download text!')

    """
    if pickle_it:
        try:
            object_to_download = pickle.dumps(object_to_download)
        except pickle.PicklingError as e:
            st.write(e)
            return None

    else:
        if isinstance(object_to_download, bytes):
            pass

        elif isinstance(object_to_download, pd.DataFrame):
            object_to_download = object_to_download.to_csv(index=False)

        # Try JSON encode for everything else
        else:
            object_to_download = json.dumps(object_to_download)

    try:
        # some strings <-> bytes conversions necessary here
        b64 = base64.b64encode(object_to_download.encode()).decode()

    except AttributeError as e:
        b64 = base64.b64encode(object_to_download).decode()

    button_uuid = str(uuid.uuid4()).replace('-', '')
    button_id = re.sub('\d+', '', button_uuid)

    custom_css = f""" 
        <style>
            #{button_id} {{
                background-color: rgb(255, 255, 255);
                color: rgb(38, 39, 48);
                padding: 0.25em 0.38em;
                position: relative;
                text-decoration: none;
                border-radius: 4px;
                border-width: 1px;
                border-style: solid;
                border-color: rgb(230, 234, 241);
                border-image: initial;

            }} 
            #{button_id}:hover {{
                border-color: rgb(246, 51, 102);
                color: rgb(246, 51, 102);
            }}
            #{button_id}:active {{
                box-shadow: none;
                background-color: rgb(246, 51, 102);
                color: white;
                }}
        </style> """

    dl_link = custom_css + f'<a download="{download_filename}" id="{button_id}" href="data:file/txt;base64,{b64}">{button_text}</a><br></br>'

    return dl_link

Gist:

4 Likes

Also, if anyone knows how to update the code to auto download the file instead of returning an anchor tag that has to then be clicked.

I was able to get the following download_link() return strings to download the file directly, but was not able to name the file with these:

return f'<iframe width="1" height="1" frameborder="0" src="data:file/txt;base64,{b64}"></iframe>'
return f'<meta name="TEST.txt" http-equiv="refresh" content="0; url=data:file/txt;base64,{b64}">'

[Update 2020-07-18] Revised the function to display a download button instead of a link after clicking a button, saving a UI step. The associated gist has been updated with the new version.

1 Like

Great work Chad, thanks for sharing!

Out of curiosity, does it have to run with a specific runtime/32 bit version?

On my windows 10 64 bit laptop, I’ve got this error:

OSError: [WinError 193] %1 is not a valid Win32 application

If not, it is possible that my environments’ve recently been messed up on that laptop - I’m planning to reinstall them all next week. :slight_smile:

Thanks,
Charly

1 Like

Thank you for the feedback @Charly_Wargnier! I’m on a Mac and don’t have a windows machine to test it on. Do you have more info on the error - e.g. line number? I might be able to add an os check update.

PS are there Streamlit best coding practices for things like os checks?

1 Like

Sure, here’s the traceback:

WinDLL(os.path.abspath(filename))
  File "c:\program files\python38\lib\ctypes\__init__.py", line 369, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: [WinError 193] %1 is not a valid Win32 application

Thanks,
Charly

1 Like

Great! Thank you @Charly_Wargnier . Is this happening when you click the download button or is it triggering an error when starting the example app?

1 Like

The latter, when trying to run streamlit run + py file.

1 Like

I looked into, but still not quite sure. Perhaps, like you said, it’s related to your environment. Keep me posted if you figure it out :slight_smile:

1 Like

Sure thing! :slight_smile:
Thanks Chad!

1 Like