Skip to content

feat: add configurable download filenames through unified controls - #559

Merged
farnabaz merged 13 commits into
vercel:mainfrom
aradhyacp:feat/make-download-file-names-customizable
Aug 24, 2026
Merged

feat: add configurable download filenames through unified controls#559
farnabaz merged 13 commits into
vercel:mainfrom
aradhyacp:feat/make-download-file-names-customizable

Conversation

@aradhyacp

@aradhyacp aradhyacp commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

Adds support for configuring custom base filenames for Streamdown's built-in download controls through the unified controls API.

The original implementation focused specifically on code block downloads and introduced a dedicated codeDownload configuration. Following maintainer feedback and guidance, the implementation was revised to provide a more consistent and extensible API across all Streamdown components that support downloads.

Download filename configuration now lives under controls, allowing consumers to configure custom filenames for code blocks, tables, and Mermaid diagrams from a single place:

<Streamdown
  controls={{
    code: { download: { filename: "myScript" } },
    table: { download: { filename: "report" } },
    mermaid: { download: { filename: "flowchart" } },
  }}
>
  {markdown}
</Streamdown>

The configured filename is treated as a base filename. Streamdown continues to determine and append the appropriate extension automatically.

For example:

  • Code: myScript.js, myScript.py, myScript.tsx, etc.
  • Table: report.csv or report.md
  • Mermaid: flowchart.svg, flowchart.png, or flowchart.mmd

When no filename is configured, the existing default filenames remain unchanged (file.<ext>, table.<ext>, and diagram.<ext> respectively).

This approach follows the maintainer's guidance to make download filename configuration part of the existing unified controls API rather than introducing a code-specific prop. It also provides a consistent foundation for download customization across the different downloadable content types supported by Streamdown.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Related Issues

Fixes #558
Closes #558
Related to #558

Changes Made

Unified download configuration

  • Removed the previously introduced codeDownload prop.
  • Added DownloadControlConfig for download-specific configuration.
  • Updated ControlsConfig to support configurable download filenames.
  • Download filename configuration is now available through controls.
  • Exposed a consistent configuration model for code, table, and Mermaid downloads.

Custom filenames

Consumers can configure a base filename using:

download: { filename: "customName" }

Supported through:

controls={{
  code: { download: { filename: "myScript" } },
  table: { download: { filename: "report" } },
  mermaid: { download: { filename: "flowchart" } },
}}

The extension continues to be inferred automatically:

  • Code blocks preserve the existing language → extension mapping.
  • Tables retain their existing CSV/Markdown download formats.
  • Mermaid retains its SVG/PNG/MMD download formats.

Existing behavior

Existing boolean configuration remains supported:

  • download: true shows the download control with the default filename.
  • download: false hides the download control.
  • code: false hides all code controls.

When no custom filename is supplied, existing default naming behavior is preserved.

Implementation

  • Added a shared getDownloadFilename utility for resolving configured filenames.
  • Updated CodeBlockDownloadButton to use the unified filename configuration.
  • Updated TableDownloadButton to use the unified filename configuration.
  • Updated MermaidDownloadDropdown to use the unified filename configuration.
  • Added fallback behavior when no custom filename is configured.
  • Removed the deprecated codeDownload property from StreamdownProps and StreamdownContext.

Tests

  • Updated tests for CodeBlockDownloadButton, TableDownloadButton, and MermaidDownloadDropdown.
  • Added coverage for configured filenames.
  • Added coverage for omitted filename configuration and fallback behavior.
  • Verified that automatic extension handling continues to work across all supported download types.

Documentation

  • Updated Streamdown configuration documentation.
  • Updated code block, table, and Mermaid documentation.
  • Added examples for custom download filenames.
  • Updated API and interactivity documentation to describe the unified controls approach.
  • Updated the changeset to describe the new controls API instead of the previous codeDownload API.

Testing

  • All existing tests pass
  • Added new tests for the changes
  • Manually tested the changes

Test Coverage

Verified custom filenames for the supported download controls:

Code:

  • myScript.js
  • myScript.py
  • myScript.tsx
  • Other extensions continue to follow the existing language mapping.

Table:

  • report.csv
  • report.md

Mermaid:

  • flowchart.svg
  • flowchart.png
  • flowchart.mmd

When no custom filename is provided, existing defaults remain unchanged:

  • file.<ext> for code
  • table.<ext> for tables
  • diagram.<ext> for Mermaid

Test component:

import { Streamdown } from "streamdown";
import { code } from "@streamdown/code";
import { mermaid } from "@streamdown/mermaid";
import "streamdown/styles.css";

const App = () => {
  const markdown = `
| Feature | Status | Notes |
|:--------|:------:|------:|
| Markdown | Supported | CommonMark compliant |
| GFM | Supported | Tables, tasks, strikethrough |
| Code highlighting | Supported | 200+ languages via Shiki |
| Math | Supported | KaTeX rendering |
| Mermaid | Supported | Flowcharts, sequences, and more |
| CJK | Supported | Chinese, Japanese, Korean |

This is a code example block:

\`\`\`javascript
console.log("Hello, world!");
\`\`\`

This is a mermaid diagram:

\`\`\`mermaid
graph TD;
    A-->B;
    A-->C;
    B-->D;
    C-->D;
\`\`\`
`;

  return (
    <div className="App">
      <div className="mx-auto mt-5 w-200">
        <Streamdown plugins={{ code, mermaid }} controls={{
          code:{
            download: { filename: "custom-code" },
          },
          mermaid: {
            download: { filename: "custom-mermaid" },
          },
          table: {
            download: { filename: "custom-table" },
          },
        }}>{markdown}</Streamdown>
      </div>
    </div>
  );
};

export default App;

Screenshots/Demos

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas where appropriate
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have created a changeset (pnpm changeset)

Changeset

  • I have created a changeset for these changes

Additional Notes

The initial version of this work introduced a dedicated codeDownload prop for configuring code block filenames.

Based on maintainer feedback, the implementation was revised so that filename customization is handled through Streamdown's existing controls configuration. This avoids introducing a separate code-specific API and makes the behavior consistent across all current downloadable content types.

The resulting API is intentionally simple:

download: { filename: "customName" }

This controls the base filename while Streamdown remains responsible for determining the appropriate extension.

The change therefore provides a unified mechanism for code, table, and Mermaid downloads while preserving the existing download behavior for consumers who do not configure a custom filename.

Future download-related customization can build on the unified controls API without requiring separate configuration props for individual content types.

…and context

- Introduced CodeDownloadConfig interface to define optional baseFileName for code downloads.
- Updated StreamdownProps to include codeDownload property.
- Enhanced StreamdownContextType to support codeDownload configuration.
- Ensured default values are set for new properties in the context.
… codeDownload configuration

- Modified filename logic in CodeBlockDownloadButton to utilize baseFileName from StreamdownContext.
- Ensured fallback to default filename if baseFileName is not provided.
…lename scenarios

- Added tests to verify the functionality of custom baseFileName in the download button.
- Included cases for handling undefined codeDownload, unknown languages, and special characters in filenames.
- Ensured that the button is enabled and the correct filename is used during the download process.
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@aradhyacp is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

…nd component usage

- Simplified the rendering of CodeBlockDownloadButton by consolidating props into a single line.
- Updated test assertions for expected save calls to improve readability and maintainability.
@farnabaz

Copy link
Copy Markdown
Collaborator

Thanks for the PR @aradhyacp,
Don't you think this should be defined in the controls: ControlsConfig props?!

@aradhyacp

Copy link
Copy Markdown
Contributor Author

Thanks for asking @farnabaz ! I did consider putting baseFileName under controls, but I kept it as a separate codeDownload prop because, based on the current definition of controls, it seems focused on configuring the UI controls themselves for example, whether the code copy/download buttons are shown:

controls={{
   table: {
      copy: true, // Show table copy button
      download: true, // Show table download button
      fullscreen: true, // Show table fullscreen button
    },
    code: {
      copy: true, // Show code copy button
      download: true, // Show code download button
    },
}}

baseFileName, on the other hand, doesn't control the UI, it configures the behavior of the download itself. So I felt keeping it as codeDownload={{ baseFileName: "customFileName" }} would keep the UI configuration and download configuration separate.

That's the reasoning behind the current API choice, but I'm happy to discuss it if you think controls is the better fit here.

@farnabaz

Copy link
Copy Markdown
Collaborator

@aradhyacp I felt the same way at first, but I think controls is the place where you configure or toggle overlay actions for all components.

In other words, having a single place to control and configure these behaviors creates better DX, since users don’t need to look through multiple props to find the right setting.

Also, imagine if we want to add similar customization for tables in the future, if we use separate props, we’ll eventually need to introduce something like a tableDownload prop as well.

@aradhyacp

Copy link
Copy Markdown
Contributor Author

Yeah, I see what you mean, especially the concern about ending up with separate props like codeDownload, tableDownload, mermaidDownload, etc. I agree that having a single place for download-related configuration would provide a better DX.

My concern is more about keeping the responsibilities of controls separate. Looking at the current ControlsConfig, it seems to represent the UI/overlay layer whether actions like copy, download, fullscreen, or panZoom are available for a component:

controls?: {
  code?: {
    copy?: boolean;
    download?: boolean;
  };
  table?: {
    copy?: boolean;
    download?: boolean;
    fullscreen?: boolean;
  };
  mermaid?: {
    download?: boolean;
    copy?: boolean;
    fullscreen?: boolean;
    panZoom?: boolean;
  };
};

Because of that, I’d prefer to keep controls focused on controlling the UI rather than mixing it with configuration for the underlying logic behavior of those actions.

Instead, perhaps we could introduce a global downloadConfig (or a better name if you have one) for download-specific behavior:

<Streamdown
  controls={{
    code: { download: true },
    table: { download: true },
  }}
  downloadConfig={{
    code: {
      baseFileName: "component",
    },
    table: {
      baseFileName: "data",
    },
  }}
/>

This would still give us a single place for download-related configuration across the different components, while keeping the UI layer separate from the underlying download behavior. It would also avoid having separate codeDownload / tableDownload / mermaidDownload props as more download customization is added.

So I agree with the underlying concern about having a single place for this configuration, I just think that place should be separate from controls to keep the UI and core behavior concerns separated.

@farnabaz

farnabaz commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

@aradhyacp For me controls is the single place to configure all control actions in all elements. How I see controls

controls?: {
  code?: boolean | {
    copy?: boolean;
    download?: boolean | { filename: string }
  };
  table?: boolean | {
    copy?: boolean;
    download?: boolean | { filename: string };
    fullscreen?: boolean;
  };
  mermaid?: boolean | {
    download?: boolean | { filename: string };
    copy?: boolean;
    fullscreen?: boolean;
    panZoom?: boolean;
  };
};

This way controls with be rich enough to support multiple use-cases. For example I would also like to ask to bring configs from your other PR into same interface #524. So we will have:

controls?: {
  code?: boolean | {
    copy?: boolean;
    download?: boolean | { filename: string };
  };
  table?: boolean | {
    copy?: boolean;
    download?: boolean | { filename: string };
    fullscreen?: boolean;
    csvSeparator: "," | ";" | "\t" | "auto" // CSV separator from #524 
  };
  mermaid?: boolean | {
    download?: boolean | { filename: string };
    copy?: boolean;
    fullscreen?: boolean;
    panZoom?: boolean;
  };
};

@aradhyacp

Copy link
Copy Markdown
Contributor Author

Thanks @farnabaz for taking the time to review both PRs and explaining the direction

I’ll update both PRs accordingly. Appreciate the guidance 🙌🏻

…wnProps

- Introduced DownloadControlConfig type to enhance download configuration options.
- Updated ControlsConfig to utilize DownloadControlConfig for download properties.
- Removed deprecated codeDownload property from StreamdownProps and context for cleaner API.
- Added a new utility function `getDownloadFilename` to retrieve configurable download filenames based on the provided controls configuration.
- Updated `CodeBlockDownloadButton`, `MermaidDownloadDropdown`, and `TableDownloadButton` components to utilize the new filename generation logic, enhancing flexibility for download filenames.
- Ensured fallback options are in place for scenarios where configuration is not defined.
… download components

- Updated tests for `CodeBlockDownloadButton`, `MermaidDownloadDropdown`, and `TableDownloadButton` to verify the use of custom filenames from controls.
- Added new test cases to ensure correct behavior when filenames are configured or omitted, including scenarios for fallback options.
- Improved assertions for clarity and maintainability across the test suite.
- Added sections on setting custom download filenames for code blocks, tables, and mermaid diagrams across multiple documentation files.
- Updated descriptions to clarify the use of the `download: { filename }` configuration option.
- Provided code examples demonstrating how to implement custom filenames in the `Streamdown` component.
- Clarified the `controls` configuration in the Streamdown component to include custom download filenames for tables, code blocks, and mermaid diagrams.
- Updated examples to demonstrate the use of `download: { filename }` for setting specific filenames during downloads.
- Enhanced descriptions in the API and features documentation to reflect the new capabilities and usage scenarios.
- Enhanced the documentation to reflect the removal of the `codeDownload` prop in favor of a unified `controls` API for customizing download filenames.
- Clarified the configuration options for downloads, including the new `download: { filename: "customName" }` format while preserving automatic file-extension mapping.
- Updated examples to demonstrate the new capabilities for code, table, and mermaid downloads.
@aradhyacp aradhyacp changed the title feat: add configurable base filename for code block downloads feat: add configurable download filenames through unified controls Aug 21, 2026
- Added .pnpm-store/ to the .gitignore file to prevent pnpm store files from being tracked in the repository.
@aradhyacp

Copy link
Copy Markdown
Contributor Author

@farnabaz Please have a look at the latest changes whenever you’re free. I’ve updated the implementation based on your guidance and moved the download filename configuration into the unified controls API, including support for code, table, and Mermaid downloads.

I’ve also updated the tests, documentation, and changeset accordingly.

Please let me know if any further changes are required. Thanks again 🤗

@farnabaz farnabaz added the minor label Aug 24, 2026

@farnabaz farnabaz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍
Thanks

@farnabaz
farnabaz merged commit b734cbf into vercel:main Aug 24, 2026
5 of 7 checks passed
lofcz added a commit to lofcz/streamdown-ng that referenced this pull request Aug 24, 2026
Keep the fork's animation timeline, animateCodeBlocks path, and ControlsConfig in streamdown-context. Take unique upstream bits: inline-code animation (vercel#595), configurable download filenames (vercel#559), and table csvSeparator (vercel#524).

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CodeBlockDownloadButton: filename/MIME not overridable (always file.<ext>, table download has filename — code doesn't)

2 participants