Can I override "code" in components with syntax highlighting?

main
1d86d8cUpdate release.yml
This post might have stale content, as main is 35 commits ahead.

can i override the "code" in components while preserving the shiki/syntax highlighting thing?

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

I'll help you understand how to override the "code" in components while preserving syntax highlighting. Let me first explore the streamdown repository structure to understand how it's organized.

Let me look at the components file more closely to understand how the code component works:

Let me check the actual directory structure:

Let me try a different approach:

Let me search for the components export more broadly:

Let me find the actual files:

Let me try to just list what's in the current directory:

Good! Now let me read the components file:

Let me check what the actual working directory is:

Let me search with a simpler pattern:

Let me use WebSearch to find information about how to override code components in streamdown while preserving syntax highlighting:

Great! I found the information. Let me look at the actual code component implementation to give you a precise answer:

Perfect! Now I have the information I need. Let me provide you with a comprehensive answer about how to override the code component while preserving syntax highlighting in Streamdown.

##Answer

Yes, you can override the code component in Streamdown while preserving syntax highlighting! Here's how:

###For Inline Code (backticks)

You can provide a custom code component that still maintains the default styling:

import { Streamdown } from "streamdown";

export default function Page() {
  const markdown = "Use `const x = 1;` in your code";

  return (
    <Streamdown
      components={{
        code: ({ children, className, ...props }) => (
          <code className={`custom-style ${className || ""}`} {...props}>
            {children}
          </code>
        ),
      }}
    >
      {markdown}
    </Streamdown>
  );
}

###For Code Blocks (syntax-highlighted blocks)

The code block component is more complex because it uses Shiki for syntax highlighting. However, you can still customize it by wrapping or modifying the CodeBlock component's behavior. The key is that code blocks automatically handle the syntax highlighting internally, so you mainly customize the styling layer:

import { Streamdown } from "streamdown";

export default function Page() {
  const markdown = `\`\`\`javascript
const x = 1;
\`\`\``;

  return (
    <Streamdown
      components={{
        pre: ({ children, ...props }) => (
          <pre
            className="custom-codeblock rounded-lg shadow-lg"
            {...props}
          >
            {children}
          </pre>
        ),
      }}
      shikiTheme={["github-light", "github-dark"]}
    >
      {markdown}
    </Streamdown>
  );
}

###Best Approaches to Preserve Syntax Highlighting

  1. Use CSS styling via data-streamdown attributes (recommended):

    [data-streamdown="code-block"] {
      border-radius: 12px;
      background: custom-color;
    }
    
  2. Override CSS variables for code blocks:

    :root {
      --muted: 210 40% 96.1%;
      --border: 214.3 31.8% 91.4%;
    }
    
  3. Customize Shiki theme instead of replacing the component:

    <Streamdown shikiTheme={["github-light", "dracula"]}>
      {markdown}
    </Streamdown>
    

###Important Notes

  • Inline code styling is simple and safe to override
  • Code blocks preserve syntax highlighting automatically - Streamdown's internal CodeBlock component handles the Shiki highlighting regardless of wrapper styling
  • The node prop is also passed to your custom component for advanced use cases
  • Use className to maintain any existing classes that Streamdown adds

So yes, you can customize the code appearance without losing the syntax highlighting! Just avoid replacing the internal logic—customize the styling layer instead.

Avatar of Julian BenegasJulian Benegascommented

r u sure? i have this setup:

const streamdownComponents: ComponentProps<typeof Streamdown>["components"] = {
  h1: (props) => <Heading level={1} {...props} />,
  h2: (props) => <Heading level={2} {...props} />,
  h3: (props) => <Heading level={3} {...props} />,
  h4: (props) => <Heading level={4} {...props} />,
  h5: (props) => <Heading level={5} {...props} />,
  h6: (props) => <Heading level={6} {...props} />,
  p: (props) => (
    <p className="my-3 leading-relaxed first:mt-0 last:mb-0" {...props} />
  ),
  a: (props) => (
    <a
      className="text-highlight-blue underline-offset-2 hover:underline"
      rel="noopener noreferrer"
      target="_blank"
      {...props}
    />
  ),
  strong: (props) => <strong className="font-semibold" {...props} />,
  em: (props) => <em className="italic" {...props} />,
  ul: (props) => <ul className="my-3 ml-4 list-disc space-y-1" {...props} />,
  ol: (props) => (
    <ol className="my-3 ml-4 list-inside list-decimal space-y-1" {...props} />
  ),
  li: (props) => <li className="pl-1" {...props} />,
  blockquote: (props) => (
    <blockquote
      className="my-3 border-faint border-l-2 pl-3 text-muted italic"
      {...props}
    />
  ),
  hr: () => <hr className="my-6 border-border-solid" />,
  code: (props) => (
    <code
      className="bg-dim/10 px-1 py-0.5 font-mono text-[0.9em] text-highlight-yellow"
      {...props}
    />
  ),
  table: (props) => (
    <div className="my-3 overflow-x-auto">
      <table className="w-full border-collapse text-sm" {...props} />
    </div>
  ),
  thead: (props) => (
    <thead className="border-border-solid border-b" {...props} />
  ),
  tbody: (props) => <tbody {...props} />,
  tr: (props) => (
    <tr className="border-border-solid border-b last:border-0" {...props} />
  ),
  th: (props) => (
    <th className="px-3 py-2 text-left font-medium text-dim" {...props} />
  ),
  td: (props) => <td className="px-3 py-2 text-muted" {...props} />,
}

and lost all syntax highlighting.

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

Ah, I see the issue! The problem is that your custom code component is handling both inline code and code blocks. In Streamdown, code blocks are rendered differently than inline code, but they both use the code element.

When you override the code component, you're losing the syntax highlighting because code blocks need to render the highlighted tokens that Shiki generates, not just plain text.

Let me search for how code blocks work in the codebase to give you the exact solution:

Let me search for how inline code is distinguished from code blocks:

Let me check the test files to understand the structure:

Let me look at the components test file directly to see how code is tested:

Let me try a different approach and search for how the code component receives its props:

Let me check the documentation for the solution:

The documentation mentions that the node prop is passed. Let me check the actual source by searching for how code blocks are detected:

Let me search the GitHub repo directly for examples:

Perfect! I found the issue. There's actually a GitHub issue (#191) about this exact problem. The problem is that the code component is used for both inline code and code blocks, so when you override it, you lose the syntax highlighting for code blocks.

The solution is to check whether the code is inline or a code block using the node prop. Here's how to fix your setup:

const streamdownComponents: ComponentProps<typeof Streamdown>["components"] = {
  // ... your other components ...
  
  code: (props) => {
    // Check if this is inline code or a code block
    // Code blocks have a parent element, inline code doesn't
    const isInlineCode = !props.node?.parent || props.node.parent.type !== 'pre';
    
    if (isInlineCode) {
      // Your custom inline code styling
      return (
        <code
          className="bg-dim/10 px-1 py-0.5 font-mono text-[0.9em] text-highlight-yellow"
          {...props}
        />
      );
    }
    
    // For code blocks, return the children as-is to preserve syntax highlighting
    // The highlighting is already applied to the children by Shiki
    return <code {...props} />;
  },
  
  // ... rest of your components ...
};

Even better approach: Use CSS to target inline code specifically:

const streamdownComponents: ComponentProps<typeof Streamdown>["components"] = {
  // ... your other components ...
  // Don't override code at all, use CSS instead
};

Then in your CSS file:

/* Style only inline code */
[data-streamdown="inline-code"] {
  @apply bg-dim/10 px-1 py-0.5 font-mono text-[0.9em] text-highlight-yellow;
}

/* Keep code blocks as default */
[data-streamdown="code-block"] {
  /* Keep default syntax highlighting */
}

The best solution (if you need full control) is to use the node prop to distinguish:

code: (props) => {
  // The node object contains metadata about the element
  // Code blocks will have children with language info
  const isCodeBlock = props.node?.type === 'code' && props.node?.lang;
  
  if (isCodeBlock) {
    // Let Shiki-highlighted code through unchanged
    return <code {...props} />;
  }
  
  // Apply your custom styling only to inline code
  return (
    <code
      className="bg-dim/10 px-1 py-0.5 font-mono text-[0.9em] text-highlight-yellow"
      {...props}
    />
  );
},

Note: There's currently an open feature request (#191) to add a dedicated inlineCode component prop to make this even easier, but for now, the CSS approach is the most reliable way to preserve syntax highlighting while customizing inline code styling.

Avatar of Julian BenegasJulian Benegascommented
  1. one
  2. two
  3. three

chief

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

I'm not sure what you're asking with that list. Are you:

  1. Testing how Streamdown renders ordered lists?
  2. Asking me to do something with those three items?
  3. Just saying "chief" as a casual greeting?

Could you clarify what you'd like help with? 😊

Avatar of Julian BenegasJulian Benegascommented

yeah i was testing! noice. can u try reading a file? like the readme.

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

Sure! Let me read the README file for you:

Hmm, let me search differently:

Great! Found them. Let me read the main README:

There seems to be an issue with the file path handling. Let me try the root README instead:

Perfect! Here's the Streamdown README:

#Streamdown

A drop-in replacement for react-markdown, designed for AI-powered streaming.

##Overview

Formatting Markdown is easy, but when you tokenize and stream it, new challenges arise. Streamdown is built specifically to handle the unique requirements of streaming Markdown content from AI models, providing seamless formatting even with incomplete or unterminated Markdown blocks.

Streamdown powers the AI Elements Message component but can be installed as a standalone package for your own streaming needs.

##Features

  • 🚀 Drop-in replacement for react-markdown
  • 🔄 Streaming-optimized - Handles incomplete Markdown gracefully
  • 🎨 Unterminated block parsing - Build with remend for better streaming quality
  • 📊 GitHub Flavored Markdown - Tables, task lists, and strikethrough support
  • 🔢 Math rendering - LaTeX equations via KaTeX
  • 📈 Mermaid diagrams - Render Mermaid diagrams as code blocks with a button to render them
  • 🎯 Code syntax highlighting - Beautiful code blocks with Shiki
  • 🛡️ Security-first - Built with rehype-harden for safe rendering
  • Performance optimized - Memoized rendering for efficient updates

Want me to read more of it or look at a specific section?

Avatar of Julian BenegasJulian Benegascommented

what about if you send out "/packages/streamdown/README.md" as the path for your read command?

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

Good idea! Let me try that:

Nice! That worked! Here's the full README:

#Streamdown

A drop-in replacement for react-markdown, designed for AI-powered streaming.

##Overview

Formatting Markdown is easy, but when you tokenize and stream it, new challenges arise. Streamdown is built specifically to handle the unique requirements of streaming Markdown content from AI models, providing seamless formatting even with incomplete or unterminated Markdown blocks.

Streamdown powers the AI Elements Message component but can be installed as a standalone package for your own streaming needs.

##Features

  • 🚀 Drop-in replacement for react-markdown
  • 🔄 Streaming-optimized - Handles incomplete Markdown gracefully
  • 🎨 Unterminated block parsing - Build with remend for better streaming quality
  • 📊 GitHub Flavored Markdown - Tables, task lists, and strikethrough support
  • 🔢 Math rendering - LaTeX equations via KaTeX
  • 📈 Mermaid diagrams - Render Mermaid diagrams as code blocks with a button to render them
  • 🎯 Code syntax highlighting - Beautiful code blocks with Shiki
  • 🛡️ Security-first - Built with rehype-harden for safe rendering
  • Performance optimized - Memoized rendering for efficient updates

##Installation

npm i streamdown

Then, update your Tailwind globals.css to include the following.

@source "../node_modules/streamdown/dist/*.js";

Make sure the path matches the location of the node_modules folder in your project. This will ensure that the Streamdown styles are applied to your project.

##Usage

You can use Streamdown in your React application like this:

import { Streamdown } from "streamdown";

export default function Page() {
  const markdown = "# Hello World\n\nThis is **streaming** markdown!";

  return <Streamdown>{markdown}</Streamdown>;
}

For more info, see the documentation.


Thanks for the tip! Using the relative path /packages/streamdown/README.md worked perfectly! 👍

Avatar of Julian BenegasJulian Benegascommented

cool. and what about "Grep README" and it not returning anything? is that expected? does Grep mathc file paths? wondering if i should create a Blob tool

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

Good question! Let me test this:

Interesting! So Grep does search file contents, not file paths. When I searched for README as a pattern, it found files that contain the text "README" in their content (like in package.json files), but it didn't match the filename itself.

The issue is that Grep searches file contents by default, not filenames. To find files by name, you need to use the glob parameter (which filters which files to search), or use List to explore directories.

So:

  • Grep = searches file contents with regex patterns
  • List = lists directory structure and finds files by name/path
  • Read = reads the full content of a specific file you already know the path to

You're right that a Blob/FileList tool could be useful if you want to:

  1. Search for files by name/pattern (not content)
  2. Get file metadata without reading the full content
  3. List files matching a glob pattern with more detail than List provides

That said, you can achieve most of what you need by combining:

  • List (with filesOnly: true and pattern) for file discovery
  • Grep (with glob parameter) to filter which files to search
  • Read (with the path from above) to get the full content

So the current tools work, but a dedicated file-search tool would definitely be more convenient! 👍


END OF POST

Log in to add a comment
Claude Haiku 4.5
Can I override "code" in components with syntax highlighting? — vercel/streamdown