Building an Adobe Express Add-on: Markdown Converter
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-sandboxThis 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 startThe 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 logicKey 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()andruntime.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/textfieldEnsure 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
.mdfileClick “Insert Markdown”
5. The styled text should appear in your document
Understanding How It Works
UI to Sandbox Communication:
ui/index.jsxwaits for the SDK to be ready, gets asandboxProxy, and passes it toAppApp.jsxcallssandboxProxy.insertMarkdownText()When the user clicks the buttonThe sandbox receives this call and executes document manipulation code
Document API Basics:
editor.createText()creates a new text nodetextNode.setPositionInParent()positions it on the pagetextNode.fullContent.applyCharacterStyles()applies formatting to text rangesinsertionParent.children.append()adds the node to the document
File Handling:
The UI uses the browser’s
FileReaderAPI to read file contentsFile 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
awaitsandbox proxy callsFile 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/