npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rn-markdown-editor

v1.1.2

Published

A fully-featured React Native Markdown Editor with toolbar, renderer, and themeable UI components

Readme

rn-markdown-editor

A fully-featured React Native Markdown Editor with a customisable toolbar, rich renderer, full-screen image viewer, and a complete UI component kit — all in one package. Works with Expo and bare React Native on iOS, Android, and Web. Supports Tailwind CSS via NativeWind for utility-class styling alongside the built-in theme system.


Table of Contents


Installation

npm install rn-markdown-editor
# or
yarn add rn-markdown-editor

Peer Dependencies

Install these if not already in your project:

npx expo install react react-native react-native-svg nativewind @react-native-async-storage/async-storage expo-file-system expo-media-library expo-video expo-image react-native-gesture-handler react-native-reanimated react-native-qrcode-svg expo-linear-gradient react-native-safe-area-context zustand @react-native-community/datetimepicker react-native-image-crop-picker expo-document-picker

Note: @expo/vector-icons is no longer required. All icons are rendered via react-native-svg using the built-in Icons component.


Import Paths

The package supports sub-path imports so you can import from granular entry points:

// ─── Root (everything) ───────────────────────────────────────────
import { MarkdownEditor, Button, useColors } from "rn-markdown-editor";

// ─── Components only ─────────────────────────────────────────────
import { MarkdownEditor, Button, Badge } from "rn-markdown-editor/components";

// ─── Hooks only ──────────────────────────────────────────────────
import { useColors, useDebouncedInput } from "rn-markdown-editor/hooks";

// ─── Types only ──────────────────────────────────────────────────
import type { ToolbarItem, ThemeColors } from "rn-markdown-editor/types";
import { DEFAULT_TOOLBAR_ITEMS } from "rn-markdown-editor/types";

// ─── Contexts only ───────────────────────────────────────────────
import { ThemeProvider, useTheme } from "rn-markdown-editor/contexts";

Exported Modules Map

| Sub-path | Exports | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rn-markdown-editor | Everything below | | rn-markdown-editor/components | Accordion, Alert, AspectRatio, Avatar, Badge, Button, Calendar, Card, Checkbox, Checkmark, Circle, ColorSwatch, ColorSwatchGroup, Container, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, EditableInput, Em, Italic, FileUpload, Flex, Float, For, Grid, GridItem, GridList, GridRoot, Heading, Highlight, Icons, ImageViewer, Input, Label, List, ListItem, ListItemIndicator, ListItemMenu, MarkdownEditor, MarkdownRenderer, MarkdownTable, Marquee, NativeSelect, NativeSelectOption, NativeSelectOptionsList, Popover, Portal, PortalHost, Progress, ProgressCircle, QRCode, Radio, RadioGroup, Radiomark, Separator, Show, Skeleton, Stack, Steps, Switch, Tabs, TabsList, TabsContent, Tag, Timeline, TimeSelector, Toast, ToastContainer, Tooltip, ToggleTip, Wrap + all prop types | | rn-markdown-editor/hooks | useColors, useDebouncedInput, useDisclosure, useGridConfig, useMergeState, useTickingValue, themeColors | | rn-markdown-editor/types | DEFAULT_TOOLBAR_ITEMS, DefaultToolbarItemId, ToolbarItem, ThemeColors, ThemeContextType | | rn-markdown-editor/contexts | ThemeProvider, useTheme, ThemeContextType, ThemeProviderProps, CustomThemeColors |

Root also exports: defaultLightColors, defaultDarkColors, ThemeColors, ThemeProviderProps, CustomThemeColors, toaster, useToastStore, usePopover, useDropdownMenu, useSteps.

Also exported from the root/components entry points — the type-only exports backing the components above:

  • Calendar: CalendarProps, CalendarSize, CalendarTranslations, CalendarVariant, DateView, FocusChangeDetails, SelectionMode, ValueChangeDetails, ViewChangeDetails, VisibleRangeChangeDetails
  • TimeSelector: TimeFormat, TimePeriod, TimeSelectorProps, TimeSelectorTranslations, TimeValueChangeDetails
  • Switch: SwitchCheckedChangeDetails, SwitchFocusChangeDetails, SwitchIds, SwitchLabelPosition, SwitchProps, SwitchRef, SwitchSize, SwitchVariant
  • Grid: GridAutoFlow, GridAutoRows, GridBreakpoint, GridItemSpec, GridPlacement, GridTemplateRow, GridVariant, BaseGridProps, ResolvedGridConfig

Installation / Setup

Wrap your root component (e.g. in App.tsx or your root layout):

import { ThemeProvider } from "./ThemeContext";
 
export default function App() {
  return (
    <ThemeProvider>
      <YourApp />
    </ThemeProvider>
  );
}

Quick Start

import {
  ThemeProvider,
  MarkdownEditor,
  MarkdownRenderer,
} from "rn-markdown-editor";
import { useState } from "react";
import { View } from "react-native";

export default function App() {
  const [text, setText] = useState("# Hello world\n\nStart writing...");

  return (
    <ThemeProvider>
      <View style={{ flex: 1, padding: 16 }}>
        <MarkdownEditor value={text} onChangeText={setText} />
        <MarkdownRenderer body={text} />
      </View>
    </ThemeProvider>
  );
}

Tailwind CSS Setup

This package supports Tailwind CSS via NativeWind, enabling utility-class styling on all components that accept a className prop (such as Card and its sub-components).

1. Install NativeWind

npx expo install nativewind tailwind-merge
npm install -D [email protected] [email protected]

2. Configure Tailwind

Add a tailwind.config.js to your project root:

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: "class",
  content: [
    "./App.tsx",
    "./components/**/*.{js,jsx,ts,tsx}",
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  presets: [require("nativewind/preset")],
  theme: {
    extend: {},
  },
  plugins: [],
};

3. Configure Babel

Update babel.config.js:

module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ["babel-preset-expo", { jsxImportSource: "nativewind" }],
      "nativewind/babel",
    ],
  };
};

4. Add the NativeWind type reference

Create or update nativewind-env.d.ts in your project root:

/// <reference types="nativewind/types" />

Dark Mode

The ThemeProvider automatically applies the .dark CSS class to <html> on web when dark mode is active, which integrates with Tailwind's darkMode: "class" strategy. On native, NativeWind's setColorScheme is used.

Using Tailwind Classes

Components that accept className (e.g. Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter) can be styled with Tailwind utilities:

import { Card, CardHeader, CardTitle, CardContent } from "rn-markdown-editor";

<Card className="mx-4 my-2 shadow-md">
  <CardHeader className="pb-2">
    <CardTitle>My Card</CardTitle>
  </CardHeader>
  <CardContent>{/* content */}</CardContent>
</Card>;

You CAN reuse cn like this:

import { Text, View } from "react-native";

import { cn } from "./cn";

export default function Button({ active }: { active: boolean }) {
  return (
    <View className={cn("p-4 rounded-lg", active && "bg-blue-500")}>
      <Text className="text-white">Click me</Text>
    </View>
  );
}

Theme System

ThemeProvider

File: src/contexts/ThemeContext.tsx

Wraps your app and provides light/dark theme context. Persists the user's choice to AsyncStorage and falls back to the system colour scheme on first launch. On web it toggles .dark on <html>; on native it syncs with NativeWind's setColorScheme.

Props (ThemeProviderProps)

| Prop | Type | Default | Description | | ------------- | --------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | children | ReactNode | — | Required. | | lightColors | CustomThemeColors | defaultLightColors | Override the light-mode palette. All keys from ThemeColors are required; extra keys are allowed. | | darkColors | CustomThemeColors | defaultDarkColors | Override the dark-mode palette. All keys from ThemeColors are required; extra keys are allowed. | | typography | Partial<TypographyConfig> | defaultTypography | Override baseFontSize and/or per-level headingSizes (used by Heading). Partial — only the keys you pass are overridden. | | container | Partial<ContainerConfig> | defaultContainer | Override padding, borderRadius, and/or containerSizes breakpoints (used by Container). Partial — only the keys you pass are overridden. |

Basic (default colours)

<ThemeProvider>{children}</ThemeProvider>

Theme Provider

Wrap your app in ThemeProvider to enable light/dark theming, custom color palettes, typography, spacing, and per-component style overrides across the whole component library.

Installation / Setup

Wrap your root component (e.g. in App.tsx or your root layout):

import { ThemeProvider } from "./ThemeContext";

export default function App() {
  return (
    <ThemeProvider>
      <YourApp />
    </ThemeProvider>
  );
}

Recommended: the config prop

The config prop is the preferred way to customize the provider. It accepts a single ThemeLibraryConfig object covering colors, typography, container sizing, root tokens, and per-component overrides — every key is optional.

import { ThemeProvider } from "./ThemeContext";
import type { ThemeLibraryConfig } from "./theme.config";

const appTheme: ThemeLibraryConfig = {
  colors: {
    lightColors: {
      background: "#FFFFFF",
      foreground: "#1A1A1A",
      primary: "#6C5CE7",
      // custom, app-specific key (allowed alongside required ThemeColors keys)
      brandAccent: "#00B894",
    },
    darkColors: {
      background: "#0D0D0F",
      foreground: "#F5F5F5",
      primary: "#A29BFE",
      brandAccent: "#00D9A5",
    },
  },

  heading: {
    baseFontSize: 16,
    headingSizes: {
      h1: { fontSize: 32, fontWeight: "800" },
      h2: { fontSize: 24, fontWeight: "700" },
    },
  },

  container: {
    padding: 20,
    borderRadius: 12,
  },

  root: {
    spacing: { sm: 8, md: 16, lg: 24 },
    radius: { sm: 4, md: 8, lg: 16 },
    focusRingColor: "#6C5CE7",
    animationDuration: 150,
  },

  components: {
    button: {
      defaultVariant: "primary",
      borderRadius: 10,
      colors: {
        primary: {
          background: "#6C5CE7",
          hoverBackground: "#5A4BD1",
          pressedBackground: "#4A3BB8",
          foreground: "#FFFFFF",
        },
      },
    },
    badge: {
      borderRadius: 999,
      fontSize: 12,
    },
    alert: {
      defaultVariant: "subtle",
      borderRadius: 8,
    },
  },
};

export default function App() {
  return (
    <ThemeProvider config={appTheme}>
      <YourApp />
    </ThemeProvider>
  );
}

Legacy props (still supported)

For simpler cases, or existing code written before config existed, you can pass individual props instead. These are ignored for any key also present in config.

<ThemeProvider
  lightColors={{ background: "#FFFFFF", primary: "#6C5CE7" }}
  darkColors={{ background: "#0D0D0F", primary: "#A29BFE" }}
  typography={{ baseFontSize: 15 }}
  container={{ padding: 20, borderRadius: 12 }}
>
  <YourApp />
</ThemeProvider>

Reading a single component's config

Inside a component, use useComponentConfig to read just the slice of config relevant to it, then merge with your own built-in defaults:

import { useComponentConfig } from "./ThemeContext";

function Button({ variant = "primary", ...props }) {
  const cfg = useComponentConfig("button");

  const borderRadius = cfg?.borderRadius ?? 8;
  const colors = cfg?.colors?.[variant];

  // ...render using borderRadius / colors
}

Notes

  • config takes precedence over the legacy lightColors / darkColors / typography / container props when both are supplied.
  • All ThemeColors keys are required in the resolved palette — the library falls back to its defaults for anything you don't override, and any extra string keys you add (e.g. brandAccent) are passed through untouched.
  • Theme selection is persisted via AsyncStorage and restored on next launch; if nothing is saved yet, it falls back to the device's system color scheme.

Custom colours

To override the palette, import the defaults and spread your changes on top. Because all ThemeColors keys are required, spreading the defaults ensures nothing is missing:

import {
  ThemeProvider,
  defaultLightColors,
  defaultDarkColors,
} from "rn-markdown-editor";

<ThemeProvider
  lightColors={{
    ...defaultLightColors,
    primary: "hsl(260, 60%, 50%)",
    accent: "hsl(330, 80%, 55%)",
    // You can also add custom keys:
    brandGradientStart: "#6366f1",
    brandGradientEnd: "#a855f7",
  }}
  darkColors={{
    ...defaultDarkColors,
    primary: "hsl(260, 60%, 70%)",
    accent: "hsl(330, 80%, 65%)",
    brandGradientStart: "#818cf8",
    brandGradientEnd: "#c084fc",
  }}
>
  {children}
</ThemeProvider>;

How it works: On mount, reads @markdown_editor_theme from AsyncStorage. If no saved value, uses useColorScheme() from React Native. Whenever the theme changes, it persists the new value and applies it to the platform (web: CSS class, native: NativeWind runtime). The resolved colour palette (user-supplied or default) is provided through context so every component and hook receives the correct colours.

CustomThemeColors

// All keys from ThemeColors are required + any additional string keys
export type CustomThemeColors = ThemeColors & { [key: string]: string };

This means:

  • ✅ All 37 built-in color tokens (background, foreground, card, ... frozen) must be provided.
  • ✅ Any extra keys you add (e.g. brandGradientStart) are passed through and accessible via useColors().

useTheme

Returns the current theme state, setters, and the resolved color palette.

import { useTheme } from "rn-markdown-editor";

function ThemeToggle() {
  const { theme, isDark, setTheme, toggleTheme, colors } = useTheme();
  return <Button onPress={toggleTheme}>{isDark ? "☀️" : "🌙"}</Button>;
}

Returns:

| Property | Type | Description | | ------------- | ------------------- | --------------------------------------------------------- | | theme | "light" \| "dark" | Current active theme | | isDark | boolean | Convenience boolean | | setTheme | (theme) => void | Set theme explicitly (persists to storage) | | toggleTheme | () => void | Toggle between light and dark | | colors | CustomThemeColors | The resolved color palette (user overrides or defaults) | | typography | TypographyConfig | Resolved typography config, used internally by Heading | | container | ContainerConfig | Resolved container config, used internally by Container |

useColors

File: src/hooks/useTheme.ts

Returns the resolved CustomThemeColors object for the current theme. If the ThemeProvider was given custom lightColors / darkColors, those are returned; otherwise the built-in defaults from theme.ts.

import { useColors } from "rn-markdown-editor";

function MyComponent() {
  const colors = useColors();
  return (
    <View
      style={{ backgroundColor: colors.card, borderColor: colors.border }}
    />
  );
}

Theme Tokens

File: src/theme.ts

Defines the ThemeColors interface and exports defaultLightColors / defaultDarkColors. Every UI component reads from these.

| Token | Purpose | | --------------------------------------------- | ---------------------------------------------------- | | background | App background | | foreground | Primary text | | card / cardForeground | Card surfaces | | popover / popoverForeground | Popover/dialog surfaces | | primary / primaryForeground | Primary action buttons | | secondary / secondaryForeground | Secondary surfaces | | muted / mutedForeground | Muted/disabled elements | | accent / accentForeground | Accent highlights | | earnings / earningsForeground | Success/earnings indicator | | grey400 | Neutral hover background | | destructive / destructiveForeground | Danger actions | | border | General borders | | input | Input field borders | | ring | Focus ring | | warning / warningForeground | Warning indicators | | sidebarBackground / sidebarForeground | Sidebar colours | | sidebarPrimary / sidebarPrimaryForeground | Sidebar primary actions | | sidebarAccent / sidebarAccentForeground | Sidebar accents | | sidebarBorder / sidebarRing | Sidebar borders & focus | | flameon | Brand/accent color (used for links, primary buttons) | | fire | Orange accent | | frozen | Blue accent |


Components

MarkdownEditor

File: src/components/MarkdownEditor.tsx

A full markdown editing experience with a scrollable toolbar, undo/redo history, image/table insert dialogs, and inline/block formatting actions.

How it works: Maintains an internal history stack (max 50 entries). Toolbar buttons call insertMarkup() for inline formatting (wraps selection in prefix/suffix) or insertBlock() for block-level formatting (inserts prefix at line start). The image and table buttons open themed Modal dialogs. Selection tracking is done via onSelectionChange to know where to insert.

Props

| Prop | Type | Default | Description | | ---------------- | ------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ | | value | string | — | Required. Current markdown text | | onChangeText | (text: string) => void | — | Required. Called on every edit | | preview | boolean | false | Hides toolbar when true | | placeholder | string | "Write your story here..." | Input placeholder | | style | StyleProp<ViewStyle> | — | Style for the text input | | containerStyle | StyleProp<ViewStyle> | — | Style for the outer container | | toolbar | DefaultToolbarItemId[] \| false | All items | Subset/order of toolbar buttons, or false to hide | | toolbarExtra | ToolbarItem[] | [] | Custom buttons appended after built-in ones | | toolbarIcons | Partial<Record<DefaultToolbarItemId, (color) => ReactNode>> | {} | Override icons for built-in actions | | onPickImage | (alt?: string) => Promise<string \| null> | — | Image picker callback. Receives the alt text entered by the user. If omitted, the upload tab is hidden | | minHeight | number | 200 | Minimum height of the text area | | fontSize | number | 16 | Font size of the text area |

Usage

<MarkdownEditor
  value={text}
  onChangeText={setText}
  toolbar={[
    "bold",
    "italic",
    "underline",
    "divider",
    "h1",
    "h2",
    "link",
    "image",
  ]}
  toolbarIcons={{
    bold: (color) => <MyBoldIcon color={color} />,
  }}
  toolbarExtra={[
    {
      id: "emoji",
      label: "Emoji",
      icon: (c) => <Text style={{ color: c }}>😊</Text>,
      onPress: () => {},
    },
  ]}
  onPickImage={async (alt) => {
    // `alt` contains the alt text the user entered in the image dialog
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: "Images",
    });
    return result.canceled ? null : result.assets[0].uri;
  }}
  minHeight={300}
  fontSize={16}
/>

Built-in Toolbar Item IDs

| ID | Action | | ---------------------------------------------------------------- | ---------------------- | | undo / redo | History navigation | | divider / divider2 / divider3 / divider4 | Visual separators | | bold / italic / underline / strikethrough / inlineCode | Inline formatting | | h1 / h2 / h3 / blockquote | Block formatting | | bulletList / orderedList | List insertion | | alignLeft / alignCenter / alignJustify | Text alignment | | link / image / table | Media & insert dialogs |


MarkdownRenderer

File: src/components/MarkdownRenderer.tsx

Renders a markdown string as native React Native views. Includes a full lexer (tokenizer) and inline renderer supporting bold, italic, underline, strikethrough, inline code, links, images, @mentions, #hashtags, code blocks, blockquotes, lists, tables, videos, pull-column layouts, and horizontal rules. Tappable images open the built-in full-screen ImageViewer.

How it works:

  1. HTML Preprocessor — Converts common HTML tags (<center>, <strong>, <table>, <img>, <video>, pull-left/pull-right column divs, etc.) to their markdown equivalents or internal tokens.
  2. Lexer — Splits the markdown into block-level Token objects (heading, paragraph, codeBlock, list, table, image, imageRow, video, columns, etc.).
  3. Block Renderers — Each token type has a memoized React component (HeadingBlock, ParagraphBlock, CodeBlock, ListBlock, ImageBlock, ImageRowBlock, VideoBlock, ColumnsBlock, etc.).
  4. Inline Renderer — Parses inline formatting within text blocks using a single regex pass, including @mention and #hashtag detection.

Props

| Prop | Type | Default | Description | | ------------------------ | ---------------------------- | ------------------- | -------------------------------------------------------------------------------- | | body | string | — | Required. Markdown string to render | | width | number | Screen width | Layout width for image/column sizing | | containerStyle | StyleProp<ViewStyle> | — | Container style | | scrollable | boolean | false | Wrap in a ScrollView | | baseFontSize | number | 14 | Base paragraph font size | | lineHeightMultiplier | number | 1.6 | Line height = fontSize × this | | colors | MarkdownColors | {} | Fine-grained color overrides (see below) | | onPressLink | (href: string) => void | Opens in browser | Link press handler | | onPressUser | (username: string) => void | — | @mention press handler | | onPressHashtag | (tag: string) => void | — | #hashtag press handler | | paddingHorizontal | number | 16 | Inner horizontal padding | | imageViewerAccentColor | string | colors.flameon | Accent color for the full-screen image viewer's download button | | numberOfLines | number | — | Clamp text to N lines (flat-text mode — non-text blocks are hidden) | | maxBodyLength | number | — | Truncate the markdown source to this many characters before rendering | | bodyColor | string | colors.foreground | Override the default text color for all rendered text | | renderTable | boolean | true | Set to false to suppress table rendering | | allowRenderImage | boolean | true | Set to false to suppress all image and image-row blocks | | renderVideo | boolean | true | Set to false to render video tokens as plain links instead of embedded players | | removeEmptyLine | boolean | false | Strip horizontal-rule tokens (useful in compact previews) | | disableLinks | boolean | false | Prevent all link/mention/hashtag tap interactions | | isImageLoading | boolean | false | Show skeleton placeholders over images while external assets load |

MarkdownColors Override

Pass a colors prop to fine-tune individual element colors without overriding the whole theme:

<MarkdownRenderer
  body={markdown}
  colors={{
    text: "#1a1a1a",
    link: "#6366f1",
    codeText: "#d97706",
    codeBackground: "#fef3c7",
    codeBlockBackground: "#1e293b",
    blockquoteBorder: "#6366f1",
    blockquoteText: "#6b7280",
    blockquoteBackground: "#f5f3ff",
    hr: "#e5e7eb",
    listMarker: "#6366f1",
    headingBorder: "#e5e7eb",
    imageCaption: "#9ca3af",
  }}
  onPressLink={(href) => openInAppBrowser(href)}
  onPressUser={(username) => navigate(`/@${username}`)}
  onPressHashtag={(tag) => navigate(`/trending/${tag}`)}
  imageViewerAccentColor="#6366f1"
/>

MarkdownColors Keys

| Key | Applies to | | ---------------------- | ------------------------------- | | text | All paragraph/inline text | | link | Hyperlinks and @mentions | | codeText | Inline code text | | codeBackground | Inline code background | | codeBlockBackground | Fenced code block background | | blockquoteBorder | Left border of blockquote | | blockquoteText | Text inside blockquote | | blockquoteBackground | Background of blockquote | | hr | Horizontal rule line color | | listMarker | Bullet/number list markers | | headingBorder | Bottom border on H1/H2 headings | | imageCaption | Image caption text below images |

Supported Markdown Syntax

| Syntax | Result | | ---------------------- | ----------------------------------- | | **bold** | bold | | *italic* | italic | | <u>underline</u> | underline | | ~~strike~~ | ~~strike~~ | | `code` | inline code | | ***bold italic*** | bold italic | | # H1 ... ###### H6 | Headings | | > blockquote | Blockquote | | ``` ... ``` | Code block (with optional lang) | | - item | Unordered list | | 1. item | Ordered list | | [text](url) | Link | | ![alt](url) | Image (tappable, opens ImageViewer) | | \| col \| col \| | Table | | --- | Horizontal rule | | :center:text | Centred line | | @username | Mention (fires onPressUser) | | #hashtag | Hashtag (fires onPressHashtag) | | !!![label](url) | Embedded video (shorthand) | | <video src="..." /> | Embedded video (HTML tag) |

Content Limiting

When used in list/feed contexts you can limit rendered output without slicing the string manually:

// Show only the first 2 lines of text, no images
<MarkdownRenderer
  body={post.body}
  numberOfLines={2}
  allowRenderImage={false}
  renderTable={false}
  renderVideo={false}
  maxBodyLength={500}
  removeEmptyLine
/>

MarkdownTable

File: src/components/MarkdownTable.tsx

A horizontally-scrollable table component with alternating row backgrounds, auto-calculated column widths, and full inline formatting support (bold, italic, code, links, images, @mentions, #hashtags) inside every cell.

How it works: Column widths are calculated based on content character length (min 80px, max 220px). The renderInline function is passed in from MarkdownRenderer so cell content stays visually in sync with the rest of the document. Header rows use the theme's secondary background; data rows alternate between card and muted.

Props

| Prop | Type | Default | Description | | ---------------- | ------------------------------------- | ------- | -------------------------------------------------------------------- | | headers | string[] | — | Required. Column header labels | | rows | string[][] | — | Required. 2D array of cell values | | renderInline | RenderInline | — | Required. Shared inline renderer from MarkdownRenderer | | cc | MarkdownColors | {} | Color overrides forwarded from the parent MarkdownRenderer | | openImage | (src: string, alt?: string) => void | no-op | Called when an image inside a cell is tapped | | onPressLink | (href: string) => void | browser | Link handler (defaults to Linking.openURL or window.open on web) | | onPressUser | (username: string) => void | — | @mention handler | | onPressHashtag | (tag: string) => void | — | #hashtag handler | | fontSize | number | 13 | Font size for all cell text |

Note: MarkdownTable is normally rendered automatically by MarkdownRenderer. Use it directly only when constructing custom table layouts.

<MarkdownTable
  headers={["Name", "Role", "Status"]}
  rows={[
    ["Alice", "Engineer", "Active"],
    ["Bob", "Designer", "**Lead**"],
  ]}
  renderInline={myRenderInline}
  onPressLink={(href) => Linking.openURL(href)}
  onPressUser={(username) => navigate(`/@${username}`)}
  onPressHashtag={(tag) => navigate(`/trending/${tag}`)}
  fontSize={14}
/>

ImageViewer

File: src/components/ImageViewer.tsx

A full-screen image viewer with pinch-to-zoom, pan, animated backdrop, and one-tap download support. Works on iOS, Android, and Web. Controlled via an imperative ref.

How it works: Mounts as a transparent Modal. The PinchPanImage inner component tracks multi-touch distances to drive a scale Animated.Value and single-finger pan while zoomed. On web, mouse-wheel events are wired up for zoom. The modal subtree is only mounted while the viewer is open, keeping gesture responders and image decoding out of the render tree when closed. Downloads use expo-file-system + expo-media-library on native (saved directly to the camera roll with a progress indicator) and a <a download> anchor on web.

Ref API (ImageViewerRef)

export interface ImageViewerRef {
  open: (src: string, alt?: string) => void;
  close: () => void;
}

| Method | Description | | ------- | --------------------------------------------------------------- | | open | Opens the viewer with the given image URL and optional alt text | | close | Closes the viewer with a fade-out animation |

Props

| Prop | Type | Default | Description | | ------------- | ------------ | ----------- | ---------------------------------------- | | accentColor | string | "#3B82F6" | Color of the download button | | onClose | () => void | — | Called after the viewer has fully closed |

Usage

import { useRef } from "react";
import { ImageViewer, ImageViewerRef } from "rn-markdown-editor";

function MyScreen() {
  const viewerRef = useRef<ImageViewerRef>(null);

  return (
    <>
      <Pressable
        onPress={() =>
          viewerRef.current?.open("https://.../photo.jpg", "A sunset")
        }
      >
        <Text>View Image</Text>
      </Pressable>

      <ImageViewer
        ref={viewerRef}
        accentColor="#6366f1"
        onClose={() => console.log("closed")}
      />
    </>
  );
}

Gestures & Interactions

| Interaction | Behaviour | | -------------------------------- | -------------------------------------------------------------------------------- | | Pinch (two fingers) | Zoom in/out (clamped 0.5× – 5×) | | Single-finger drag | Pan when zoomed in | | Tap on backdrop | Reset zoom to 1× | | Mouse wheel (web) | Zoom in/out | | Download button | Saves to camera roll (native) or triggers download (web) with progress indicator | | Hardware back / onRequestClose | Closes the viewer |


Skeleton

File: src/components/Skeleton.tsx

A simple animated placeholder component that pulses between full opacity and 40% opacity on a loop. Use it to indicate loading state for content areas.

Props

| Prop | Type | Default | Description | | -------------- | ---------------------- | -------- | ---------------------------- | | style | StyleProp<ViewStyle> | — | Additional style overrides | | width | number \| string | "100%" | Width of the skeleton block | | height | number \| string | — | Height of the skeleton block | | borderRadius | number | 6 | Corner radius |

// Single line placeholder
<Skeleton height={16} width="60%" />

// Card placeholder
<Skeleton height={120} borderRadius={12} style={{ marginBottom: 8 }} />

Button

File: src/components/Button.tsx

A theme-aware pressable button with hover/press state tracking and 5 variants × 5 sizes. Supports both string children and render-prop children for full control.

How it works: Uses Pressable with onHoverIn/Out and onPressIn/Out to track interaction state. Each variant defines default colors for normal/hover/pressed states. All color props can be overridden individually.

Variants (5)

| Variant | Normal | Hover | Use case | | ----------- | ------------------------ | ------------------------ | ----------------- | | primary | flameon bg, white text | 80% opacity bg | Primary CTA | | secondary | secondary bg | grey400 bg | Secondary actions | | outline | Transparent bg, border | flameon bg, white text | Bordered actions | | ghost | Transparent | grey400 bg | Subtle actions | | link | No bg, flameon text | Underlined text | Inline links |

Sizes (5)

| Size | Padding | | --------- | --------- | | default | 16h × 8v | | sm | 12h × 6v | | lg | 32h × 12v | | icon | 8 all | | none | 0 |

Props

| Prop | Type | Default | | ----------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------- | | variant | "primary" \| "secondary" \| "outline" \| "ghost" \| "link" | "primary" | | size | "default" \| "sm" \| "lg" \| "icon" \| "none" | "default" | | textColor / hoveredTextColor / pressedTextColor | string | Auto from variant | | backgroundColor / hoveredBackgroundColor / pressedBackgroundColor | string | Auto from variant | | borderColor / hoveredBorderColor / pressedBorderColor | string | Auto from variant | | children | ReactNode \| ((state) => ReactNode) | — |

// String children
<Button variant="primary" size="default" onPress={save}>Save</Button>

// Render-prop children
<Button variant="outline">
  {({ textColor, hovered, pressed }) => (
    <Text style={{ color: textColor }}>Custom</Text>
  )}
</Button>

Badge

File: src/components/Badge.tsx

A small label/tag component with 7 variants. Renders as a pill-shaped View with auto text styling. Supports string, ReactNode, or render-prop children.

Variants (7)

| Variant | Background | Text | | ------------- | -------------------- | ----------------------- | | default | primary | primaryForeground | | secondary | secondary | secondaryForeground | | accent | accent | accentForeground | | warning | warning | foreground | | destructive | destructive | destructiveForeground | | success | earnings | earningsForeground | | outline | Transparent + border | foreground |

Props

| Prop | Type | Default | | ------------- | ---------------------------------------------------- | ----------------- | | variant | See above | "default" | | textColor | string | Auto from variant | | bgColor | string | Auto from variant | | borderColor | string | Auto from variant | | children | ReactNode \| ((props: { textColor }) => ReactNode) | — |

<Badge variant="accent">New</Badge>
<Badge variant="success">Published</Badge>
<Badge bgColor="#f0abfc" textColor="#4a044e">Custom</Badge>

Alert

File: src/components/Alert.tsx

A Chakra-style alert banner with a semantic status, an overridable colorPallete, four visual variants, three sizes, an optional status icon, and an optional dismiss button.

How it works: status (info/warning/success/error/neutral) picks both a default color pallete and a default icon. colorPallete overrides just the color, independent of status. Each variant derives its background/border/text/icon colors from the pallete's HSL base at a fixed lightness, so every pallete×variant combination stays legible without hand-tuned color tables. Pass children as a render-prop ({ textColor, iconColor }) => ReactNode to build fully custom content that still inherits the resolved theme colors.

Props

| Prop | Type | Default | Description | | -------------------------------------------------- | -------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------- | | status | "info" \| "warning" \| "success" \| "error" \| "neutral" | "info" | Picks the default colorPallete and status icon | | colorPallete | "gray" \| "red" \| "orange" \| "yellow" \| "green" \| "teal" \| "blue" \| "cyan" \| "purple" \| "pink" | From status | Overrides the color derived from status | | variant | "subtle" \| "surface" \| "outline" \| "solid" | "subtle" | Visual treatment | | size | "sm" \| "md" \| "lg" | "md" | Controls padding, gaps, icon size, and font sizes | | close | boolean | false | Shows a dismiss (×) button | | onClose | () => void | — | Fires when the close button is pressed | | title | ReactNode | — | Bold title line (ignored if children is provided) | | description | ReactNode | — | Body text (ignored if children is provided) | | icon | ReactNode \| false | — | Custom icon; pass false to render no icon | | textColor / bgColor / borderColor / iconColor | string | Auto | Override the resolved variant/pallete colors individually | | children | ReactNode \| ((props: { textColor: string; iconColor: string }) => ReactNode) | — | Full custom content instead of title/description |

<Alert status="success" title="Saved" description="Your changes were saved." />

<Alert status="error" variant="solid" close onClose={() => setVisible(false)}>
  {({ textColor }) => <Text style={{ color: textColor }}>Something went wrong.</Text>}
</Alert>

Input

File: src/components/Input.tsx

A themed TextInput wrapper with focus ring styling, a built-in library of typed input behaviors (number/decimal/email/phone/password/date/time/datetime), and optional start/end elements. Applies theme colors automatically and removes the web outline.

How it works: Wraps React Native TextInput with forwardRef. Tracks focus state to toggle borderColor between colors.border (unfocused) and colors.ring (focused). On web, sets outlineStyle: "none" to replace the browser default with the custom border. The type prop drives three things at once: which keyboard opens (keyboardType), live sanitization of keystrokes (e.g. stripping non-digits for "number"), and which built-in icon/affordance is attached (eye toggle for password, calendar/clock for date/time types). Date/time types additionally run a live input mask keyed off format (tokens: YYYY, MM, DD, HH/hh, mm, ss, A), parsing a fully-typed masked string back into a Date for onDateChange.

Props

| Prop | Type | Default | Description | | ---------------------------------------- | --------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | type | InputType | "text" | "text" | "text-number" | "number" | "decimal" | "password" | "email" | "phone" | "date" | "time" | "datetime" | | variant | InputVariant | "outline" | "outline" (bordered) | "filled" | "transparent" (no border/background) | | startElement / endElement | ReactNode | — | Custom content before/after the text field; overrides the automatic type icon | | startElementStyle / endElementStyle | StyleProp<ViewStyle> | — | Style for the start/end element wrapper | | containerStyle | StyleProp<ViewStyle> | — | Style for the outer bordered container | | mainStyle | StyleProp<ViewStyle> | — | Style for the inner TextInput itself | | required | boolean | false | Forwarded as aria-required on web only (a11y-only, doesn't block submission) | | readOnly | boolean | false | Fully locks the field; takes precedence over typeable | | typeable | boolean | true | Date/time only: false makes the field picker-only (tap anywhere opens the picker, typing is blocked) | | selectable | boolean | true | Date/time only: whether tapping the field/icon opens the picker | | format | string | per-type (see below) | Date/time display + typed-mask pattern, e.g. "DD/MM/YYYY" or "hh:mm A". 12h vs 24h is inferred from hh/A in the string | | minimumDate / maximumDate | Date | — | Bounds for the date/time picker | | dateValue | Date | — | Controlled Date backing date/time/datetime types | | onDateChange | (date: Date) => void | — | Fires with the parsed Date once a full value is typed or picked | | min / max | number | — | Number/decimal only: clamps the value on blur | | minLength | number | — | Non-numeric, non-date types: flags an error border on blur if under length (doesn't block typing) | | ...rest | TextInputProps (minus editable) | — | Spread onto the underlying TextInput |

Default format per type: date"YYYY-MM-DD", time"HH:mm", datetime"YYYY-MM-DD HH:mm".

<Input value={value} onChangeText={setValue} placeholder="Type here..." />

<Input type="password" placeholder="Password" />

<Input
  type="date"
  format="DD/MM/YYYY"
  dateValue={date}
  onDateChange={setDate}
  minimumDate={new Date(2020, 0, 1)}
/>

EditableInput

File: src/components/EditableInput.tsx

A text field that starts out as a locked, borderless label and switches into an editable Input when the user double-taps / double-clicks it. Editing ends on blur by default.

How it works: Renders a single Input and swaps its variant/typeable/readOnly props based on internal isEditing state — previewVariant (default "transparent") while locked, editVariant (default "outline") while editing. While not editing, an invisible Pressable overlay sits on top of the input to catch the double-tap (native, via tap-timing) or dblclick (web). Exposes an imperative handle (edit, cancel, focus, blur) via ref for programmatic control, e.g. an external "Edit" button.

Props

| Prop | Type | Default | Description | | ------------------ | ----------------------------- | ----------------- | ------------------------------------------------------------------------ | | value | string | — | Required. Controlled text value | | onChange | (text: string) => void | — | Required. Fires on every keystroke while editing | | onSubmit | (value: string) => void | — | Fires with the final value when editing ends (if submitOnBlur) | | onCancel | (value: string) => void | — | Fires when editing is cancelled (Escape on web) | | disabled | boolean | false | Blocks entering edit mode entirely | | previewVariant | InputVariant | "transparent" | Input variant shown while not editing | | editVariant | InputVariant | "outline" | Input variant shown while editing | | submitOnBlur | boolean | true | Automatically exit edit mode (and fire onSubmit) on blur | | containerStyle | ViewStyle | — | Style for the wrapping container | | ...rest | InputProps (minus variant, typeable, readOnly, type, onChange) | — | Spread onto the underlying Input |

Ref (EditableInputRef): edit(), cancel(), focus(), blur()

const ref = useRef<EditableInputRef>(null);

<EditableInput
  ref={ref}
  value={title}
  onChange={setTitle}
  onSubmit={(final) => saveTitle(final)}
/>
<Button size="sm" onPress={() => ref.current?.edit()}>Rename</Button>

PinInput

File: src/components/PinInput.tsx

A row of individually-focused boxes for OTP / PIN entry, with auto-advance, backspace-to-previous, paste/autofill distribution across boxes, and masking.

How it works: Renders count TextInputs (each capped to a single visible character) inside a shared View. Typing a character auto-focuses the next box; backspace on an empty box focuses and clears the previous one. Pasting or autofilling a multi-character string into one box distributes it across the remaining boxes starting from that index. mask renders secureTextEntry per filled box. otp sets autoComplete="one-time-code" (web) / textContentType="oneTimeCode" (iOS) so the OS can offer SMS-code autofill.

Props

| Prop | Type | Default | Description | | ----------------------- | ---------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------- | | value / defaultValue | string[] | — | Controlled / uncontrolled per-box values | | count | number | value?.length ?? 4 | Number of boxes to render | | type | "numeric" \| "alphanumeric" \| "alphabetic" | "numeric" | Sets keyboard type and the default per-character regex | | pattern | string | — | Custom regex string overriding type's default character check | | mask | boolean | false | Renders filled boxes as secureTextEntry, like type="password" | | otp | boolean | false | Enables OS one-time-code autofill hints | | colorPalette | PinInputColorPalette | "gray" | Accent color for the focused/invalid border (gray|red|orange|yellow|green|teal|blue|cyan|purple|pink) | | size | "2xs" \| "xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" | "md" | Box dimensions and font size | | variant | "outline" \| "subtle" \| "flushed" | "outline" | Box border/background treatment | | attached | "true" \| "false" | "false" | "true" renders boxes edge-to-edge sharing borders instead of spaced apart | | invalid | boolean