Building an Adobe Express Add-on: Markdown Converter

Fardeen Mansoori

Nov 3, 2025

7 min read

Blog Image

This guide walks you through creating a simple add-on that converts Markdown files to styled text in Adobe Express documents. After completing this tutorial, you’ll understand the fundamentals needed to build your own add-ons.


Prerequisites

  • Adobe Express account (create one at new.express.adobe.com)

  • Node.js 18 or higher

  • Basic knowledge of React and JavaScript

Step 1: Enable Add-on Development

1. Open new.express.adobe.com in your browser

2. Click your avatar icon in the top right, then the gear icon to open Settings

3. Enable “Add-on Development” (you may need to accept Developer Terms)

4. Close Settings

We already discussed these things how to set up the development environment and many other things, in the previous blog; you can check it out here (https://www.fardeen.me/blogs/cmhg3u1630000aprc4jz7d0ly).

Step 2: Create Your Add-on Project

Open your terminal and run:

npx @adobe/create-ccweb-add-on markdown-to-text --template react-javascript-with-document-sandbox

This creates a React-based add-on template with Document Sandbox support. Navigate to the project:

cd markdown-to-text
npm install
npm run build
npm run start

The server will start and display a local URL.

Step 3: Load Your Add-on in Adobe Express

1. Open new.express.adobe.com

2. Create or open a document

3. Click the Add-ons icon in the left rail

4. Enable “Add-On Testing”

5. Paste the local URL from your terminal

6. Your add-on panel will appear on the right side

Step 4: Understand the Project Structure

The template creates this structure:

src/
├── index.html # Entry point HTML
├── manifest.json # Add-on configuration
├── ui/
│ ├── index.jsx # React app entry (sets up SDK connection)
│ └── components/
│ └── App.jsx # Your main UI component
└── sandbox/
└── code.js # Document manipulation logic

Key concepts:

  • UI (iframe): Your React app runs here. This is where users interact.

  • Document Sandbox: Code that manipulates the Adobe Express document runs here.

  • Communication: The UI and Sandbox communicate via proxies using runtime.apiProxy() and runtime.exposeApi().

The ui/index.jsx file handles the SDK initialization and passes the sandboxProxy to your App component as a prop.

Step 5: Install Additional Components

We’ll need these components. Install it:

npm install @swc-react/progress-circle @swc-react/field-label @swc-react/textfield

Ensure all @swc-react/* packages use the same version (check package.json).

Step 6: Build the UI

Open src/ui/components/App.jsx. Replace the default button example with this file upload interface:

import "@spectrum-web-components/theme/express/scale-medium.js";
import "@spectrum-web-components/theme/express/theme-light.js";

import { Button } from "@swc-react/button";
import { Theme } from "@swc-react/theme";
import { ProgressCircle } from "@swc-react/progress-circle";
import { FieldLabel } from "@swc-react/field-label";
import { Textfield } from "@swc-react/textfield";
import React, { useState, useCallback, useRef } from "react";
import "./App.css";

const App = ({ addOnUISdk, sandboxProxy }) => {
    const [file, setFile] = useState(null);
    const [fileContent, setFileContent] = useState(null);
    const [processing, setProcessing] = useState(false);
    const [title, setTitle] = useState("");
    
    const fileInputRef = useRef(null);

    const isMarkdownFile = (file) => {
        return (
            file.name.toLowerCase().endsWith(".md") ||
            file.type === "text/markdown"
        );
    };

    const handleFile = useCallback((selectedFile) => {
        if (!selectedFile) return;

        if (!isMarkdownFile(selectedFile)) {
            alert("Please select a Markdown (.md) file");
            return;
        }

        setFile(selectedFile);

        const reader = new FileReader();
        reader.onload = (e) => {
            setFileContent(e.target.result);
        };
        reader.onerror = () => {
            alert("Error reading file");
        };
        reader.readAsText(selectedFile);
    }, []);

    const handleFileSelect = useCallback((event) => {
        const selectedFile = event.target.files?.[0];
        handleFile(selectedFile);
    }, [handleFile]);

    const handleInsert = useCallback(async () => {
        if (!fileContent || !sandboxProxy) return;

        setProcessing(true);
        try {
            await sandboxProxy.insertMarkdownText(fileContent, title || null);
        } catch (error) {
            console.error("Error inserting markdown:", error);
            alert("Failed to insert markdown. Check console for details.");
        } finally {
            setProcessing(false);
        }
    }, [fileContent, title, sandboxProxy]);

    return (
        <Theme system="express" scale="medium" color="light">
            <div className="container">
                <h2>Markdown to Text</h2>

                <FieldLabel for="title-input">Document Title (optional)</FieldLabel>
                <Textfield
                    id="title-input"
                    placeholder="Enter title"
                    value={title}
                    onInput={(e) => setTitle(e.target.value)}
                    style={{ width: "100%", marginBottom: "16px" }}
                />

                <div
                    style={{
                        border: "2px dashed #ccc",
                        borderRadius: "8px",
                        padding: "24px",
                        textAlign: "center",
                        marginBottom: "16px",
                        cursor: "pointer",
                        backgroundColor: file ? "#f0f0f0" : "transparent",
                        minHeight: "100px",
                        display: "flex",
                        flexDirection: "column",
                        justifyContent: "center",
                        alignItems: "center"
                    }}
                    onClick={() => {
                        const fileInput = document.getElementById("file-input");
                        if (fileInput) {
                            fileInput.click();
                        }
                    }}
                >
                    {file ? (
                        <div>
                            <p style={{ fontWeight: "bold", marginBottom: "8px" }}>File Selected</p>
                            <p style={{ fontSize: "14px", color: "#666" }}>
                                {file.name}
                            </p>
                            <p style={{ fontSize: "12px", color: "#999", marginTop: "8px" }}>
                                Click to select a different file
                            </p>
                        </div>
                    ) : (
                        <div>
                            <p style={{ fontWeight: "bold", marginBottom: "8px" }}>Select Markdown File</p>
                            <p style={{ fontSize: "14px", color: "#666", marginTop: "8px" }}>
                                Click here to choose a .md file
                            </p>
                        </div>
                    )}
                    <input
                        ref={fileInputRef}
                        type="file"
                        id="file-input"
                        accept=".md,.markdown"
                        onChange={handleFileSelect}
                        style={{ display: "none" }}
                    />
                </div>

                <Button
                    variant="accent"
                    onClick={handleInsert}
                    disabled={!fileContent || processing}
                    style={{ width: "100%" }}
                >
                    {processing ? "Processing..." : "Insert Markdown"}
                </Button>

                {processing && (
                    <div style={{ marginTop: "16px", textAlign: "center" }}>
                        <ProgressCircle size="s" indeterminate />
                    </div>
                )}
            </div>
        </Theme>
    );
};

export default App;

This UI:

  • Allows users to select a Markdown file

  • Reads the file content using the FileReader API

  • Optionally accepts a document title

  • Calls the sandbox proxy to insert the content when the button is clicked

Step 7: Implement Document Sandbox Logic

Open src/sandbox/code.js. Replace the createRectangle function with markdown insertion logic:

import addOnSandboxSdk from "add-on-sdk-document-sandbox";
import { editor, constants } from "express-document-sdk";

const { runtime } = addOnSandboxSdk.instance;

function start() {
    const sandboxApi = {
        insertMarkdownText: (markdownText, title) => {
            // Parse simple markdown patterns
            const lines = markdownText.split(/\r?\n/);
            const insertionParent = editor.context.insertionParent;
            
            // Combine title and content
            let fullText = "";
            if (title) {
                fullText = title + "\n\n";
            }
            
            // Convert markdown to plain text (simple version)
            let plainText = fullText;
            for (const line of lines) {
                // Remove markdown syntax for basic formatting
                let processedLine = line
                    .replace(/^#{1,6}\s+/, "") // Remove heading markers
                    .replace(/\*\*(.+?)\*\*/g, "$1") // Remove bold markers
                    .replace(/\*(.+?)\*/g, "$1") // Remove italic markers
                    .replace(/`(.+?)`/g, "$1"); // Remove code markers
                
                if (processedLine.trim()) {
                    plainText += processedLine + "\n";
                } else {
                    plainText += "\n";
                }
            }

            // Create text node
            const textNode = editor.createText(plainText.trim());

            // Position text on the page
            textNode.setPositionInParent(
                { x: 50, y: 50 },
                { x: 0, y: 0 }
            );

            // Apply basic styling
            textNode.fullContent.applyCharacterStyles(
                { fontSize: 16 },
                { start: 0, length: textNode.fullContent.text.length }
            );

            // Style the title if present
            if (title) {
                const titleLength = title.length;
                textNode.fullContent.applyCharacterStyles(
                    { fontSize: 24, fontWeight: 700 },
                    { start: 0, length: titleLength }
                );
            }

            // Apply heading styles (lines starting with #)
            let currentOffset = title ? title.length + 2 : 0;
            for (const line of lines) {
                const match = line.match(/^(#{1,6})\s+(.+)$/);
                if (match) {
                    const level = match[1].length;
                    const headingText = match[2];
                    const fontSize = 28 - (level * 2);
                    
                    textNode.fullContent.applyCharacterStyles(
                        { fontSize, fontWeight: 700 },
                        { start: currentOffset, length: headingText.length }
                    );
                    currentOffset += headingText.length + 1;
                } else if (line.trim()) {
                    currentOffset += line.length + 1;
                } else {
                    currentOffset += 1;
                }
            }

            // Add to document
            insertionParent.children.append(textNode);
        }
    };

    runtime.exposeApi(sandboxApi);
}

start();

This implementation:

  • Receives markdown text and optional title from the UI

  • Parses basic markdown patterns (headings, bold, italic)

  • Creates a text node in the document

  • Applies character styles for headings and titles

  • Inserts the text at position (50, 50) on the page

Step 8: Test Your Add-on

1. Save all files. The webpack dev server should auto-reload

2. In Adobe Express, refresh your add-on panel (or reload it)

3. Create or open a document

4. In the add-on panel:

  • Optionally enter a title

  • Select a .md file

  • Click “Insert Markdown”

5. The styled text should appear in your document

Understanding How It Works

UI to Sandbox Communication:

  • ui/index.jsx waits for the SDK to be ready, gets a sandboxProxy, and passes it to App

  • App.jsx calls sandboxProxy.insertMarkdownText() When the user clicks the button

  • The sandbox receives this call and executes document manipulation code

Document API Basics:

  • editor.createText() creates a new text node

  • textNode.setPositionInParent() positions it on the page

  • textNode.fullContent.applyCharacterStyles() applies formatting to text ranges

  • insertionParent.children.append() adds the node to the document

File Handling:

  • The UI uses the browser’s FileReader API to read file contents

  • File reading happens in the UI (iframe), not in the sandbox

  • Only the text content is passed to the sandbox

Next Steps

Now that you have a working add-on, you can extend it:

1. Enhanced Markdown Parsing: Add support for lists, links, and code blocks

2. Better Styling: Use fonts from the Document API (`fonts.fromPostscriptName()`)

3. Paragraph Styles: Apply paragraph-level formatting for lists and spacing

4. Error Handling: Add better user feedback for errors

5. Multiple Formats: Support exporting or importing other file types

Key Takeaways

  • UI handles user interaction: File selection, forms, buttons — all in React

  • Sandbox handles document manipulation: Creating nodes, applying styles, positioning

  • Communication is async: Always await sandbox proxy calls

  • File reading happens in UI: Use the FileReader API before sending to the sandbox

  • Text API is powerful: You can style specific character ranges and paragraphs

You now have the foundation to build your own Adobe Express add-ons. Explore the Document API reference to discover more capabilities.

See you in the next one!!!

Follow me on x dot com: https://x.com/fardeentwt
See all my Add-Ons here: https://addon.fardeen.me/