When to use
Markdown is a document markup language that can implement basic common rich text functions such as titles, pictures, tables, links, bolding, etc. through simple tags. MDX is based on Markdown and allows the introduction of JSX to achieve more complex and customized document writing and display requirements.
The MarkdownRender component provided by Semi supports rendering Markdown and MDX. No special configuration is required. By passing in plain text, rich text content that conforms to Semi style specifications can be rendered. Code blocks are highlighted by CodeHighlight; register the languages they use with CodeHighlight.register first, an unregistered language is shown as plain text.
Usually used in the following scenarios:
- Document site writing and rendering
- Front-end rendering when the server dynamically generates rich text content
- A light interactive website that focuses on content display
Demos
How to import
<script setup lang="ts">
import { MarkdownRender } from '@fibrous/semi-ui-vue';
</script>
Basic usage
After importing MarkdownRender, just pass in Markdown or MDX plain text directly.
Note that symbols such as < and { are valid JSX syntax and are treated as code, so they cannot be rendered directly and must be escaped with \. If you only need to render pure Markdown, see the Simple Markdown section below.
Code
<script setup lang="ts">
import { MarkdownRender } from '@fibrous/semi-ui-vue';
const raw = `
正文内容是普通的文本,也可以**加粗**~~删除线~~和<u>下划线</u> [超链接](https://semi.design) 等 Markdown 与 HTML 的基本语法所支持的富文本,也支持 emoji 🍰
部分符号需要转义 \\{\\} \\<\\> ...
<br/>
<br/>
---
#### Semi Design DSM
[Semi DSM](https://semi.design/dsm) 是 Semi Design 提供的设计系统管理工具(Design System Management),支持全局、组件级别的样式定制,并在 Figma 和前端代码之间保持同步\\
适用于各种规模的团队,无论你是需要简化工作流程,提高团队协作,还是增加生产力,我们都有适合你的功能
##### 中大型企业
- 多达 3000+ Design Token,深入每一处细节的定制可能,色彩,阴影,边距,圆角,动效,渲染结构均可自由定制,告别 ~~CSS 硬编码~~
- 功能强大,经过抖音内部数千项目验证过的 UI lib,轻松应对各类复杂场景
- A11y 无障碍友好,国际化功能完备
- 面向社区建设,完全开源,无使用限制
- 从 designOps 到 devOps,自动化工作流,Figma UI Kit 一键刷入主题,生成 Style Guideline,研发一行 npm 代码配置接入
##### 初创企业
- 无需从 0 到 1 投入大量研发资源,快速复用开源社区优秀方案, 低成本快速定制具备品牌特色的设计系统。
- 一键支持暗色模式生成,支持根据品牌色快速生成包含 320 个全色阶、兼容深/浅两种模式的色彩系统,并支持动态切换
- 不断进化,DSM + Semi Design 组件由<u>抖音前端架构团队</u>专业维护,已稳定迭代五年+,值得信赖
##### 自由设计师/个人开发者
- 低成本快速创建风格各异的设计系统,更少时间,更快交付
- 研发接入友好,无需反复沟通,交付npm包产物,一键完成代码接入

---
#### MarkdownRender 渲染列表语法
- 好好地吃饭
- 好好地睡觉
- 好好地游玩
- 好好地学习
- 好好地聊天
- 好好地吵架
- 过着平凡普通的每日
| 支持 | Markdown 表格 | c | d |
| - | :- | -: | :-: |
| 1 | 2 | 3 | 4 |
| 21 | 22 | 23 | 24 |
| 31 | 32 | 33 | 34 |
| 41 | 42 | 43 | 44 |
`;
</script>
<template>
<MarkdownRender :raw="raw" />
</template>Modify element style
You can arbitrarily replace the display effect of document elements in Markdown or MDX documents. Just pass your Vue component override to the components prop. An element's attributes (such as class and colspan) reach the component under their HTML names and are read from attrs; its content arrives in the default slot.
For example, now you need to set the color of all level 2 headings to the main color
Code
<script setup lang="ts">
import { h, type FunctionalComponent } from 'vue';
import { MarkdownRender, TypographyTitle, type MarkdownRenderComponents } from '@fibrous/semi-ui-vue';
// `components` takes component objects, which a template cannot define inline.
const PrimaryTitle: FunctionalComponent = (_, { slots }) => h(TypographyTitle, { heading: 2, style: { color: 'var(--semi-color-primary)' } }, () => slots.default?.());
const components: MarkdownRenderComponents = { h2: PrimaryTitle };
</script>
<template>
<MarkdownRender :components="components" raw="## 从 Semi Design 到 Any Design 快速定义你的设计系统,并应用在设计稿和代码中" />
</template>Any element tag that Markdown generates can be overridden; the common ones are a blockquote br code del em h1 h2 h3 h4 h5 h6 hr img li ol p pre strong ul table
Simple Markdown
When the Markdown you render is just pure markdown without any JSX code, you can pass format="md" to enable Markdown-only mode. In this mode, you don't need to escape special characters.
无需转义的符号{}<> ...
Code
<script setup lang="ts">
import { MarkdownRender } from '@fibrous/semi-ui-vue';
</script>
<template>
<MarkdownRender format="md" raw="无需转义的符号{}<> ..." />
</template>format="md" renders through a unified pipeline: remark-parse parses the source, remark-gfm (when remarkGfm is true) and remarkPlugins run in order, remark-rehype turns it into an HTML syntax tree, rehypePlugins run, and hast-util-to-jsx-runtime creates the Vue virtual nodes with components applied. No code is generated or evaluated, so it renders under a CSP without unsafe-eval, and the MDX compiler is never loaded.
format="mdx" compiles the source to code with @mdx-js/mdx and evaluates it at runtime, so the CSP has to allow unsafe-eval; @mdx-js/mdx is loaded on demand the first time MDX is rendered.
<script setup lang="ts">
import { MarkdownRender } from '@fibrous/semi-ui-vue';
import rehypeRaw from 'rehype-raw';
const rehypePlugins = [rehypeRaw];
</script>
<template>
<MarkdownRender format="md" raw='<span style="color:red">Red text</span>' :rehypePlugins="rehypePlugins" />
</template>
Add custom components
By passing in custom components to the components prop, you can write JSX directly in Markdown, and the component will be rendered to the final page, supporting JS events. The default Markdown components can be obtained from MarkdownRender.defaultComponents and can be used for secondary packaging.
JSX attributes in MDX reach the component by Vue's rules: className joins class, and style can be a string. Listeners are passed as onClick, onChange, onInput, onFocus and onBlur, which the component receives through emit or attrs; other events use Vue's names, such as onMouseenter, onKeydown and onDblclick.
Code
<script setup lang="ts">
import { h, type FunctionalComponent } from 'vue';
import { Button, MarkdownRender, type MarkdownRenderComponents } from '@fibrous/semi-ui-vue';
// `components` takes component objects, which a template cannot define inline. The MDX source refers to it by name;
// the onClick it passes falls through to the Button.
const MyButton: FunctionalComponent = (_, { slots }) => h(Button, { type: 'primary', style: { marginBottom: '12px' } }, () => slots.default?.());
const components: MarkdownRenderComponents = { ...MarkdownRender.defaultComponents, MyButton };
const raw = `
#### 下面是一个渲染在 Markdown 中的按钮
<MyButton onClick={()=>alert("点击了 MyButton")}>MyButton 点我</MyButton>
直接在 Markdown 中书写 JSX 即可
`;
</script>
<template>
<MarkdownRender :components="components" :raw="raw" />
</template>When you replace the built-in code component, the source of a fenced block arrives as text nodes in the default slot; pass the nodes the default slot returns to codeText to get the source as is, with the trailing newline and without trimming. The built-in components can also be imported on their own from deep paths:
import { codeText } from '@fibrous/semi-ui-vue/markdown-render/index';
import { code, pre } from '@fibrous/semi-ui-vue/markdown-render/components/index';
Add plugins
Remark and Rehype plugins of the unified ecosystem are supported through remarkPlugins rehypePlugins, please refer to MDXJS and unified for details
The source compiles again when raw, format, remarkGfm or an entry of a plugin array changes; a components change only renders again. Plugin arrays are compared entry by entry, so an array literal in the template does not compile again while its entries stay the same. The options object of a [plugin, options] entry is compared by reference too: written as a template literal it compiles again on every render, so define it in script setup to keep it stable:
<script setup lang="ts">
import { MarkdownRender } from '@fibrous/semi-ui-vue';
import type { MarkdownRenderProps } from '@fibrous/semi-ui-vue';
import remarkToc from 'remark-toc';
defineProps<{ raw: string }>();
const remarkPlugins: MarkdownRenderProps['remarkPlugins'] = [[remarkToc, { heading: 'Contents' }]];
</script>
<template>
<MarkdownRender format="md" :raw="raw" :remarkPlugins="remarkPlugins" />
</template>
Compile errors
When the MDX has a syntax error or a plugin throws, the last successfully rendered content stays on the page and the error is logged with console.error in development; no unhandled Promise rejection is left behind.
Accessibility
With aria-label or aria-labelledby and no role, the root element gets role="group"; a role you pass takes precedence. An image inside a link cannot be previewed, so the link is the only interactive element; the caption under an image is hidden from assistive technology and the image's name comes from alt alone.
API
MarkdownRender has no default slot and no events; class, style, id, data-* and listeners land on the root div.
Props
| Property | Description | Type | Default |
|---|---|---|---|
| raw | Markdown or MDX source text. Compiles again on change. | string | undefined |
| components | Overrides Markdown elements and adds the custom components MDX sources refer to. Values are Vue components or HTML tag names; an element key such as h1 takes a component or its own tag name. Merged over MarkdownRender.defaultComponents; a change only renders again, it does not compile again. | MarkdownRenderComponents | semiGlobalDefault("MarkdownRender", "components") |
| format | Format of raw: md is plain Markdown, mdx allows embedded JSX. Compiles again on change. | "md" | "mdx" | semiDefault("MarkdownRender", "format", "mdx") |
| remarkPlugins | Custom Remark plugins. Compiles again when an entry changes. | PluggableList | semiGlobalDefault("MarkdownRender", "remarkPlugins") |
| rehypePlugins | Custom Rehype plugins. Compiles again when an entry changes. | PluggableList | semiGlobalDefault("MarkdownRender", "rehypePlugins") |
| remarkGfm | Whether to enable GitHub GFM syntax. Compiles again on change. | boolean | semiDefault("MarkdownRender", "remarkGfm", true) |
Events
| Event | Description | Callback parameters |
|---|---|---|
| None |
Slots
| Slot | Description | Scope parameters |
|---|---|---|
| None |
Exposes
| Name | Description | Type |
|---|---|---|
| None |
Design Token
| Sass variable | Value | Usage |
|---|---|---|
| $spacing-markdownRender_component_image-marginLeft | 16px | |
| $spacing-markdownRender_component_image-marginRight | 16px | |
| $spacing-markdownRender_component_image-marginTop | 16px | |
| $spacing-markdownRender_component_image-marginBottom | 16px | |
| $width-markdownRender_component_image-maxWidth | 50% | |
| $width-markdownRender_component_image-maxHeight | 500px | |
| $spacing-markdownRender_component_image_alt-marginTop | 8px | |
| $spacing-markdownRender_component_header1-marginTop | 16px | |
| $spacing-markdownRender_component_header1-marginBottom | 16px | |
| $spacing-markdownRender_component_header2-marginTop | 16px | |
| $spacing-markdownRender_component_header2-marginBottom | 16px | |
| $spacing-markdownRender_component_header3-marginTop | 16px | |
| $spacing-markdownRender_component_header3-marginBottom | 16px | |
| $spacing-markdownRender_component_header4-marginTop | 16px | |
| $spacing-markdownRender_component_header4-marginBottom | 16px | |
| $spacing-markdownRender_component_header5-marginTop | 16px | |
| $spacing-markdownRender_component_header5-marginBottom | 16px | |
| $spacing-markdownRender_component_header6-marginTop | 16px | |
| $spacing-markdownRender_component_header6-marginBottom | 16px | |
| $spacing-markdownRender_component_list-paddingLeft | 24px | |
| $spacing-markdownRender_component_list-marginTop | 16px | |
| $spacing-markdownRender_component_list-marginBottom | 16px | |
| $color-markdownRender_simpleCode-bg | var(--semi-color-fill-0) | |
| $color-markdownRender_simpleCode-text | var(--semi-color-text-1) | |
| $color-markdownRender_component_list | var(--semi-color-text-0) | |
| $color-markdownRender_component_image_alt | var(--semi-color-tertiary) |