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.0.20

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.


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 react-native-gesture-handler react-native-reanimated

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 | Badge, Button, Input, Icons, ImageViewer, MarkdownTable, MarkdownEditor, MarkdownRenderer, Skeleton + all prop types | | rn-markdown-editor/hooks | useColors, useDebouncedInput, useDisclosure, useMergeState, 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.


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>
  );
}

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. |

Basic (default colours)

<ThemeProvider>{children}</ThemeProvider>

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) |

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>

Input

File: src/components/Input.tsx

A themed TextInput wrapper with focus ring styling. 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.

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

Accepts all standard TextInputProps.


Icons

File: src/components/Icons.tsx

42 SVG icons built with react-native-svg. No external icon font required — @expo/vector-icons is not a dependency. Each icon is created via a makeIcon() factory that reads the current theme color for its default stroke.

Icon Props

| Prop | Type | Default | | ------------- | -------- | -------------------------------- | | size | number | 24 | | color | string | colors.foreground (from theme) | | fill | string | "none" | | strokeWidth | number | 2 |

<Icons.Bold size={20} color="#000" />
<Icons.Image size={24} />
<Icons.Link size={18} color="blue" />

Available Icons (42)

Toolbar: Undo, Redo, Bold, Italic, Underline, Strikethrough, Code, Heading1, Heading2, Heading3, Quote, List, ListOrdered, AlignLeft, AlignCenter, AlignRight, AlignJustify, Link, Image, Table, Upload, Minus

Navigation: ChevronLeft, ChevronRight, ChevronDown, ChevronUp, ArrowLeft

Actions: Cross, Check, Search, More, Eye, Send, Save, Plus, ExternalLink

Social / Status: Heart, MessageCircle, Bell, Settings, AlertCircle


Hooks

useDebouncedInput

File: src/hooks/useDebouncedInput.ts

Provides an immediate value (for the input field) and a debounced value (for API calls). Built on top of useMergeState for performance.

const { immediateValue, debouncedValue, setInput, clearDebounce } =
  useDebouncedInput("", 300);

// In a search input:
<Input value={immediateValue} onChangeText={setInput} />;

// Use debouncedValue for API calls:
useEffect(() => {
  searchAPI(debouncedValue);
}, [debouncedValue]);

| Param | Type | Default | Description | | -------------- | -------- | ------- | -------------- | | initialValue | string | "" | Starting value | | delay | number | 300 | Debounce ms |

Returns:

| Property | Type | Description | | ---------------- | ------------------------- | -------------------------------------- | | immediateValue | string | Updates instantly on every keystroke | | debouncedValue | string | Updates after delay ms of inactivity | | setInput | (value: string) => void | Updates both values | | clearDebounce | () => void | Cancels pending debounce timer |


useDisclosure

File: src/hooks/useDisclosure.ts

Manages open/close boolean state with optional callbacks. Useful for modals, drawers, dropdowns.

const { isOpen, onOpen, onClose, onToggle } = useDisclosure({
  defaultIsOpen: false,
  onOpen: () => console.log("Opened"),
  onClose: () => console.log("Closed"),
});

<Button onPress={onToggle}>Toggle Menu</Button>;
{
  isOpen && <MyModal onClose={onClose} />;
}

| Option | Type | Default | | --------------- | ------------ | ------- | | defaultIsOpen | boolean | false | | onOpen | () => void | — | | onClose | () => void | — | | onToggle | () => void | — |


useMergeState

File: src/hooks/useMergeState.ts

A useState replacement that shallow-merges partial updates instead of replacing. Similar to class component this.setState().

const [state, setState] = useMergeState({ name: "", age: 0, loading: false });

// Update only one field:
setState({ loading: true });

// Functional update:
setState((prev) => ({ age: prev.age + 1 }));

Signature: useMergeState<T>(initialState: T) => [T, (partial: Partial<T> | (prev: T) => Partial<T>) => void]


Types

File: src/types/toolbar.ts

| Export | Kind | Description | | ----------------------- | --------- | ----------------------------------------------------------------------------------- | | DefaultToolbarItemId | type | Union of all 22 built-in toolbar button IDs | | ToolbarItem | interface | Shape for custom toolbar items: { id, type?, label?, icon?, onPress?, disabled? } | | DEFAULT_TOOLBAR_ITEMS | const | Array of all default IDs in display order |

import { DEFAULT_TOOLBAR_ITEMS } from "rn-markdown-editor/types";
import type { ToolbarItem, DefaultToolbarItemId } from "rn-markdown-editor/types";

// Create a custom item:
const myItem: ToolbarItem = {
  id: "my-btn",
  label: "My Action",
  icon: (color) => <Icons.Plus size={16} color={color} />,
  onPress: () => doSomething(),
};

File Structure

src/
├── index.ts                    # Main entry — re-exports everything
├── theme.ts                    # ThemeColors interface + light/dark defaults
├── components/
│   ├── index.ts                # Barrel: all components
│   ├── Badge.tsx               # Badge component (7 variants)
│   ├── Button.tsx              # Button component (5 variants × 5 sizes)
│   ├── Icons.tsx               # 42 SVG icons via react-native-svg
│   ├── ImageViewer.tsx         # Full-screen pinch-zoom image viewer
│   ├── Input.tsx               # Themed TextInput wrapper
│   ├── MarkdownEditor.tsx      # Full editor with toolbar + dialogs
│   ├── MarkdownRenderer.tsx    # Markdown → native view renderer
│   ├── MarkdownTable.tsx       # Scrollable themed table
│   └── Skeleton.tsx            # Animated loading placeholder
├── contexts/
│   ├── index.ts                # Barrel: ThemeProvider, useTheme
│   └── ThemeContext.tsx        # Theme context + AsyncStorage persistence
├── hooks/
│   ├── index.ts                # Barrel: all hooks
│   ├── useDebouncedInput.ts    # Debounced input hook
│   ├── useDisclosure.ts        # Open/close state hook
│   ├── useMergeState.ts        # Shallow-merge useState
│   └── useTheme.ts             # useColors hook
└── types/
    ├── index.ts                # Barrel: all types + toolbar constants
    └── toolbar.ts              # Toolbar item IDs, ToolbarItem interface

License

MIT