Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Data Display

Markdown

Data Display

Markdown React component for Tailwind CSS v4

Log ViewerPresence
Live props
Content
children
Appearance
size
variant
caret
tone
color
codeAccent
codeLineNumbers
State
streaming
Layout
codeMaxHeight480px
Behavior
headingIds
linkTarget
allowImages
breaks
Code
import { Markdown } from "@/components/ui/markdown"

<Markdown>
  I found **3 overdue invoices** in September, **$22,775.95** in total:

| Client | Invoice | Amount | Late |
|---|---|---:|---:|
| Acme Corp | #2026-0405 | $12,480.00 | 1 day |
| Greenleaf Pharmacy | #2026-0371 | $7,315.20 | 53 days |
| West Side Books | #2026-0366 | $2,980.75 | 57 days |

**What I suggest:**
1. A polite reminder to Acme Corp, the due date passed yesterday.
2. A call to Greenleaf Pharmacy: Daniel Reed, `(503) 555-0118`.
3. For West Side Books, a formal notice. I can use the **Payment notice** template.

Shall I draft the emails?
</Markdown>
  1. Home
  2. Components
  3. Data Display
  4. Markdown
Pro

Markdown React component for Tailwind CSS v4

v1.0.1·Updated in 1.10.0

A React Markdown renderer in the library's look: scrolling tables, highlighted code, task lists and a flicker-free streaming mode for AI.

Read it like any page: headings keep their levels, links are always underlined (external ones get a small arrow and open in a new tab), done tasks are struck through, and fenced code gets a highlighted block with a copy button. A table wider than the column scrolls sideways inside its own rounded box with soft edge fades, never the page; once it overflows it is a focusable region, so Tab reaches it and the arrow keys scroll it. Tab also stops on each link (Enter follows it), on every code block's scroller and on its copy button. An image that fails to load shows its alt text instead of a broken icon. With streaming on, text arrives without flicker: half-typed bold, code spans, links and table headers stay hidden until they complete, only the last block re-renders, each new block fades in and a caret pulses at the end of the text. Press Replay as a stream in the playground to replay the answer. On a phone the text stays at 15px, long words and links wrap, and tables and code scroll with a swipe. Reduced motion drops the fade and stills the caret.

Markdown is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add markdown, together with Code Block, useScrollEdges, Tones and UIProvider and the npm packages react-markdown, remark-gfm and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add markdown

Markdown is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import Markdown from @/components/ui/markdown.

import { useEffect, useRef, useState } from "react"
import { Sparkles } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Markdown } from "@/components/ui/markdown"
import { mockStream } from "@/lib/ai-chat"

const answer = `I found **3 overdue invoices**, **$22,775.95** in total:

| Client | Invoice | Amount |
|---|---|---:|
| Acme Corp | #2026-0405 | $12,480.00 |
| Greenleaf Pharmacy | #2026-0371 | $7,315.20 |

Shall I draft the emails?`

export function AssistantAnswer() {
  const [text, setText] = useState("")
  const [streaming, setStreaming] = useState(false)
  const stream = useRef<AbortController | null>(null)
  useEffect(() => () => stream.current?.abort(), [])

  async function generate() {
    stream.current?.abort()
    const controller = new AbortController()
    stream.current = controller
    setText("")
    setStreaming(true)
    for await (const delta of mockStream(answer, { signal: controller.signal })) setText((t) => t + delta)
    // a restart aborted this stream: the new one owns the flag
    if (stream.current === controller) setStreaming(false)
  }

  return (
    <div className="flex flex-col gap-3">
      <Button variant="soft" size="sm" leftIcon={<Sparkles />} onClick={generate}>
        Generate
      </Button>
      <Markdown streaming={streaming} variant="compact">
        {text}
      </Markdown>
    </div>
  )
}

Props

  • childrendefault "I found **3 overdue invoices** in September, **$22,775.95** in total: | Client | Invoice | Amount | Late | |---|---|---:|---:| | Acme Corp | #2026-0405 | $12,480.00 | 1 day | | Greenleaf Pharmacy | #2026-0371 | $7,315.20 | 53 days | | West Side Books | #2026-0366 | $2,980.75 | 57 days | **What I suggest:** 1. A polite reminder to Acme Corp, the due date passed yesterday. 2. A call to Greenleaf Pharmacy: Daniel Reed, `(503) 555-0118`. 3. For West Side Books, a formal notice. I can use the **Payment notice** template. Shall I draft the emails?"
    string | number | null

    The markdown source (GitHub flavored: tables, task lists, strikethrough, autolinks, footnotes). null, undefined and an empty string render nothing; other values go through String(). Raw HTML in the source is never rendered.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Text size and line height: sm 14 / 24px (side panels, reasoning), md 15 / 28px (chat, notes, comfortable on a phone), lg 16 / 28px (documents). Headings, code and tables scale with it.

  • variantdefault "prose"
    "prose" | "compact"

    prose is a document's rhythm: blocks 1em apart and headings up to 1.6em. compact is a chat's rhythm: blocks 0.75em apart and headings never above 1.2em, so an answer never shouts.

  • streamingdefault false
    boolean

    Text is still arriving. Half-typed syntax is repaired (completeMarkdown), only the last top-level block re-parses per chunk, new blocks fade in, a caret shows at the end and the root gets aria-busy. Turn it off when the answer completes: the whole source then renders in one pass.

  • caretdefault "dot"
    "dot" | "bar" | "block" | "none"

    The caret at the end of streaming text: a pulsing 10px dot, a thin text-high bar, a block cursor, or none. It sits inline after the last word, and on a line of its own after a code block.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    Colors the links, the checked task boxes and the blockquote rail. neutral keeps links in the text color with a soft underline that darkens on hover.

  • colordefault ""
    string

    Any CSS color. Replaces the tone for links, task boxes and the quote rail, mixed for contrast in both themes.

  • codeAccentdefault ""
    string

    The accent of fenced code blocks, a hex color that the whole code theme is built from. Empty keeps code-block's own; set it app-wide with UIProvider defaults={{ "code-block": { accent } }}.

  • codeLineNumbersdefault false
    boolean

    A line-number gutter in fenced code blocks.

  • codeMaxHeightdefault 480
    number

    The tallest a code block gets, in px; longer code scrolls inside its block. 0 lets it grow with the code.

  • headingIdsdefault false
    boolean

    Gives h1 to h4 an id from slugifyHeading (What's new becomes whats-new, repeats get -2, -3), for #anchors and a table of contents. Ids a plugin already set are kept.

  • linkTargetdefault "auto"
    "auto" | "_blank" | "_self"

    auto opens absolute http(s) links in a new tab with rel noopener noreferrer nofollow and keeps relative links and #anchors in place; _blank opens every link but #anchors in a new tab; _self never does. Links that open a new tab show a small arrow and say so to screen readers.

  • allowImagesdefault true
    boolean

    false renders every image as a link to it with its alt text, so untrusted model output can never load a tracking pixel by itself.

  • breaksdefault false
    boolean

    A single newline becomes a line break, as in chat apps and GitHub comments, so signatures, addresses and verse keep their lines. Off follows CommonMark, where a single newline is a space. Code keeps its newlines either way.

  • components
    Components (react-markdown)

    Replace any element, e.g. render crm://client/c01 links as chips. Yours win over the built-in look; each receives the element's props plus the hast node. A code override also receives fenced code (className language-x), as in react-markdown. Keep the object stable (module scope or useMemo), or every block re-renders.

  • remarkPlugins
    PluggableList

    Remark plugins, appended after remark-gfm (always on, single tildes stay text). Keep the array stable.

  • rehypePlugins
    PluggableList

    Rehype plugins, run before the heading ids and the caret. Keep the array stable.

  • urlTransform
    (url: string, key: string, node: Element) => (string | null | undefined)

    Runs on every href and src first. The default, react-markdown's defaultUrlTransform, drops javascript: and other unsafe protocols. An empty result renders the link text without a link. Allow your own scheme with url.startsWith("crm://") ? url : defaultUrlTransform(url).

  • onLinkClick
    (href: string, event: MouseEvent<HTMLAnchorElement>) => void

    Called on every link click, for in-app navigation; call event.preventDefault() to stop the browser. The newest handler is used, so an inline function is fine.

  • labels
    Partial<MarkdownLabels>

    Every string it reads out: streaming "Generating" (sr-only after the caret), image "Image", taskDone "Done", taskOpen "Not done", code (language) => `${language} code`, newTab "(opens in a new tab)", footnotes "Footnotes", footnoteBack (index) => `Back to reference ${index}`.

  • classNames
    Partial<Record<MarkdownSlot, string>>

    Extra classes for the parts: heading, paragraph, link, list, listItem, blockquote, code (the block around a CodeBlock), inlineCode, tableWrapper (the bordered box), table, hr, image, caret.

  • className
    string

    Extra classes for the root, merged last. The root spreads every other div prop (id, aria-*, data-*).

Parts

  • MarkdownLink

    component

    The built-in link (target, rel, the new-tab arrow and its sr-only text, onLinkClick), exported so a components.a override can hand back the links it does not handle: href?.startsWith("crm://") ? <Chip /> : <MarkdownLink {...props} />. Props: MarkdownLinkProps.

  • completeMarkdown

    function

    The streaming repair, pure: closes an open code fence, appends closers for unmatched ** __ ~~ ` * _ (snake_case and 2*3 stay as they are), drops a marker with nothing after it, turns a half-typed [text](htt into text, holds a table back until its separator row arrives and drops half-typed list markers, # and >. Never touches code. Signature: (text: string) => string.

  • splitMarkdownBlocks

    function

    Splits a source into top-level blocks at blank lines, keeping code fences, loose lists and indented continuations whole. The unit the streaming memo works in. Signature: (text: string) => string[].

  • markdownToText

    function

    Plain text for copying and screen reader announcements: syntax stripped, list items on their own lines (bullets become •), table cells joined with tabs, code kept as written. Signature: (text: string) => string.

  • slugifyHeading

    function

    The id headingIds uses: lowercase, accents folded, anything but letters and digits turned into dashes (What's new in 4.2 becomes whats-new-in-4-2). Signature: (text: string) => string.

  • DEFAULT_MARKDOWN_LABELS

    constant

    The English strings, to spread under your own or translate once in UIProvider defaults. Type: MarkdownLabels.

Examples

Streaming

The email answer streams letter by letter: bold, the quote and the SQL fence never show raw, the caret follows the text, and breaks keeps the signature on two lines.

Document

size lg, prose and headingIds for release notes, with a table of contents fed by slugifyHeading.

Code heavy

Bash, TypeScript and JSON fences with a brand accent and line numbers; long code scrolls in its block.

Untrusted output

allowImages off and linkTarget _blank: the pixel becomes a link, javascript: loses its link, raw HTML renders nothing.

Sizes and variants

sm, md and lg side by side; switch between the compact chat rhythm and the prose document rhythm.

Custom components

components.a turns crm:// links into client chips and hands every other link back to MarkdownLink; urlTransform lets the scheme through.

Dependencies

react-markdownremark-gfmlucide-reactcode-blockuse-scroll-edgestonesui-config

Files

  • components/ui/markdown.tsx

FAQ

Is the React Markdown component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Markdown?

Run npx wingo-ui@latest add markdown. It also adds the Wingo UI items it builds on (Code Block, useScrollEdges, Tones and UIProvider) and the npm packages react-markdown, remark-gfm and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/markdown.

How does Markdown behave on phones?

On a phone the text stays at 15px, long words and links wrap, and tables and code scroll with a swipe.

See also

Code BlockTable Of ContentsCollapsibleuseScrollEdgeshookAI Chatlib

Used in

File SystemChat MessageChat ThreadReasoning IndicatorAI Chat PageBlog Post PageHelp Article Page

More React data display components

PresenceSchedulerTagTimelineTree ViewKbd
All 23 React data display components

Articles using this component

  • React Rich Text Editor: Tiptap, Markdown and Mentions

    Component guides · Oct 19, 2026 · 9 min read

  • Nextjs llms.txt: Generate llms-full.txt and .md Pages

    Building UI with AI agents · Oct 16, 2026 · 10 min read

  • React Markdown Streaming Without Flicker or Scroll Jumps

    Building UI with AI agents · Oct 16, 2026 · 10 min read

PreviousLog ViewerNext Presence
Northwind assistantNorthwind Pro · today, 4:32 PM

I found 3 overdue invoices in September, $22,775.95 in total:

ClientInvoiceAmountLate
Acme Corp#2026-0405$12,480.001 day
Greenleaf Pharmacy#2026-0371$7,315.2053 days
West Side Books#2026-0366$2,980.7557 days

What I suggest:

  1. A polite reminder to Acme Corp, the due date passed yesterday.
  2. A call to Greenleaf Pharmacy: Daniel Reed, (503) 555-0118.
  3. For West Side Books, a formal notice. I can use the Payment notice template.

Shall I draft the emails?

streaming=false · 4 blocks
Internal noteEmma Carter · Sep 24, 2026

Acme Corp · collection plan

Contact: Nathan Price, finance director. See the client record (opens in a new tab). Office: 28 Market St, San Francisco

  • Invoice #2026-0405 issued on Sep 10, 2026
  • Past due since Sep 23, 2026
  • Email reminder today, with the standard template
  • Call on Friday if the payment does not arrive

Usually pays 2 or 3 days late. No formal notice yet.

The Acme Corp office in San Francisco

sql
SELECT number, amount, due_date
FROM invoices
WHERE client_id = 'c01' AND status = 'overdue';

Replay the answer to watch half-typed bold and the table header wait until they are complete.