rn-markdown-editor
v1.1.2
Published
A fully-featured React Native Markdown Editor with toolbar, renderer, and themeable UI components
Maintainers
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
- Import Paths
- Quick Start
- Tailwind CSS Setup
- Theme System
- Components
- MarkdownEditor
- MarkdownRenderer
- MarkdownTable
- ImageViewer
- Skeleton
- Button
- Badge
- Alert
- Input
- EditableInput
- PinInput
- Icons
- Accordion
- Avatar
- Card
- Radio
- Checkbox
- Rating
- Slider
- ColorSwatch / ColorSwatchGroup
- ColorPicker
- Center / Square
- Container
- Stack
- Flex
- Wrap
- AspectRatio
- Separator
- Circle
- Float
- Heading
- Label
- Highlight
- Em
- List
- For
- Checkmark
- Radiomark
- Tabs
- Tag
- TagsInput
- Timeline
- Toast
- Progress
- ProgressCircle
- QRCode
- Marquee
- Steps
- Tooltip
- Portal
- SimplePopover
- Popover
- ActionBar
- FloatingPanel
- DropdownMenu
- FileUpload
- Select
- Splitter
- NativeSelect
- Show
- Switch
- Calendar
- TimeSelector
- Grid (Root / Item / List)
- Hooks
- Types
- License
Installation
npm install rn-markdown-editor
# or
yarn add rn-markdown-editorPeer 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-pickerNote:
@expo/vector-iconsis no longer required. All icons are rendered viareact-native-svgusing the built-inIconscomponent.
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
configtakes precedence over the legacylightColors/darkColors/typography/containerprops when both are supplied.- All
ThemeColorskeys 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
AsyncStorageand 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_themefrom AsyncStorage. If no saved value, usesuseColorScheme()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 viauseColors().
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:
- 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. - Lexer — Splits the markdown into block-level
Tokenobjects (heading, paragraph, codeBlock, list, table, image, imageRow, video, columns, etc.). - Block Renderers — Each token type has a memoized React component (
HeadingBlock,ParagraphBlock,CodeBlock,ListBlock,ImageBlock,ImageRowBlock,VideoBlock,ColumnsBlock, etc.). - 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 |
|  | Image (tappable, opens ImageViewer) |
| \| col \| col \| | Table |
| --- | Horizontal rule |
| :center:text | Centred line |
| @username | Mention (fires onPressUser) |
| #hashtag | Hashtag (fires onPressHashtag) |
| !! | 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:
MarkdownTableis normally rendered automatically byMarkdownRenderer. 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
