vercel / vercel/streamdown

Make tailwind optional

Open
#543 0 comments 8 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
5.6k
Forks
302
Avg merge
3d 19h
Merged PRs (30d)
14

Description

Problem Statement

I don't use tailwind, and I don't want to introduce tailwind into a project that uses a completely different design system, just for one component.

I've seen #13 and I am already using the package without tailwind.

But why does it need to be tailwind by default?

It doesn't look like there is any real dependency on tailwind at all.

Proposed Solution

If you would drop the tailwind requirement and make it optional, we could have a smaller bundle size.

The default components for all standard HTML elements can literally just be:

({ node, children }) =>
  createElement(node.tagName, node.properties, children)

This would be totally fine for most projects - many sites will already have a default typography, and this will "just work", and will look like the rest of the content on the web site.

Ideally, I would suggest you should export two components, e.g. Streamdown for tailwind users (to retain backwards compatibility) and StreamdownUnstyled for the rest of us.

With a bit of internal refactoring, Streamdown can essentially be a wrapper around StreamdownUnstyled, such that tree-shaking can shed the tailwind overhead from the bundle.

With an internal factory function for the component, you could easily omit tailwind-merge from the unstyled component and shed this from the bundle as well.

As an side, the className property doesn't work very well, probably not even for tailwind users - setting it to hello produces a class attribute like:

space-y-4 whitespace-normal [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 hello

Even tailwind users will need to undo this styling by overriding each of the properties - and the rest of us just have these unused classnames in our resulting HTML. I think it would be simpler for everyone if className would replace rather than add to the attribute. Anyhow, that's just my opinion - if there was an unstyled version of the component, this wouldn't matter to me.

Lastly, I would suggest adding a defaultComponent property - since we have the allowedTags property, which can allow tags that don't have a defined render-function in components, this would act as a fallback for those. It would also mean that the default configuration for the unstyled component can literally just be the function I showed above, with nothing configured for components by default at all.

Alternatives Considered

This is the obvious answer:

              <Streamdown
                key={index}
                className="markdown-content"
                components={{
                  h1: ({ children }) => <h1>{children}</h1>,
                  h2: ({ children }) => <h2>{children}</h2>,
                  h3: ({ node, children }) =><h3>{children}</h3>,
                  h4: ({ children }) => <h4>{children}</h4>,
                  h5: ({ children }) => <h5>{children}</h5>,
                  h6: ({ children }) => <h6>{children}</h6>,
                  p: ({ children }) => <p>{children}</p>,
                  strong: ({ children }) => <strong>{children}</strong>,
                  em: ({ children }) => <em>{children}</em>,
                  ul: ({ children }) => <ul>{children}</ul>,
                  ol: ({ children }) => <ol>{children}</ol>,
                  li: ({ children }) => <li>{children}</li>,
                  a: ({ children }) => <a>{children}</a>,
                  code: ({ children }) => <code>{children}</code>,
                  pre: ({ children }) => <pre>{children}</pre>,
                  blockquote: ({ children }) => (
                    <blockquote>{children}</blockquote>
                  ),
                  table: ({ children }) => <table>{children}</table>,
                  thead: ({ children }) => <thead>{children}</thead>,
                  tbody: ({ children }) => <tbody>{children}</tbody>,
                  tr: ({ children }) => <tr>{children}</tr>,
                  th: ({ children }) => <th>{children}</th>,
                  td: ({ children }) => <td>{children}</td>,
                  img: () => <img />,
                  hr: () => <hr />,
                  sup: ({ children }) => <sup>{children}</sup>,
                  sub: ({ children }) => <sub>{children}</sub>,
                  section: ({ children }) => <section>{children}</section>,
                }}>
                {part.text}
              </Streamdown>

It's a lot of boilerplate just to remove tailwind. (and as said, it doesn't quite work.)

And we're still shipping all the default tailwind components.

Use Case
The use case should be obvious for anything who isn't using tailwind. 😊
Priority

Important

Contribution
  • I am willing to help implement this feature
Additional Context

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the Streamdown export and current default component configuration, then trace how components, className, allowedTags, and tailwind-merge enter the bundle. Define the unstyled export or factory and fallback behavior while preserving the existing Streamdown API; done should include coverage for rendering without Tailwind and for custom className and defaultComponent behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
tailwindcss, typescript
Domain
frontend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.