src/content/post/turning-natspec-into-markdown-ui.mdx 9.8 K raw
1
---
2
title: "Turning Solidity NatSpec into Interactive Markdown UI"
3
publishDate: "31 Aug 2025"
4
description: "An exploration on how NatSpec could be used to not only maintain context but provide user interfaces"
5
tags: ["programming", "blockchain"]
6
ogImage: "../../assets/blog-images/natspec-contract.png"
7
atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvuqmxen2v"
8
---
9
10
![cover](../../assets/blog-images/natspec-contract.png)
11
12
One of the most common problems encountered when building decentralized applications is the disconnect between the smart contract and the client. A normal flow might look something like this:
13
14
```
15
solidity -> byte code & abi -> EVM <- byte code <- abi <- client
16
```
17
18
Thanks to the ABI (Application Binary Interface) that is generated at compile time we have instructions we can use in clients to interact with smart contracts. It's been an essential piece for years as we've built apps that interact with smart contracts. More recently at OpenZeppelin we released an open source tool called the [Contracts UI Builder](https://builder.openzeppelin.com) which makes it even easier to build UI forms for contracts. Despite how useful ABI has been, it does miss one important piece: context. An ABI field might look something like this:
19
20
```json
21
{
22
  "type": "function",
23
  "name": "setNumber",
24
  "inputs": [
25
    {
26
      "name": "newNumber",
27
      "type": "uint256",
28
      "internalType": "uint256"
29
    }
30
  ],
31
  "outputs": [],
32
  "stateMutability": "nonpayable"
33
}
34
```
35
36
All we know about this function is it probably sets a new number, but why? What is the number for? We might be able to answer these questions by looking at the source code of the contract itself, but there are many cases where we have no idea what a paramter is used for. This is something that [Seb brought up](https://farcaster.xyz/seb/0xf5694e6e) on Farcaster this weekend, stating "There should be some sort of universal markup language for every smart contract (that isn't an ABI) that allows anyone to easily interact onchain."
37
38
This got me wondering if the NatSpec could be used to help solve this problem. If you're not familiar with it, the [NatSpec](https://docs.soliditylang.org/en/latest/natspec-format.html) works a lot like JSDoc where the developer can leave comments in a particular format that can be used by the compiler to create documentation or even SDKs and CLIs. It's been in Solidity for years and has actually been used by OpenZeppelin's documentation to generate API references. While most of the tags handle things like parameters or returns, the `@notice` tag can be used as a general description and be filled with whatever we want to write, so why not markdown? It doesn't stop there though. What if we could build entire UIs out of the NatSpec? Thanks to a new library / proposed standard called [Markdown UI](https://markdown-ui.com/) I was able to build a [MVP](https://natspec-ui.orbiter.website) of this idea, and in this post I'll show you how it works!
39
40
The first thing you need to do is write up the markdown as NatSpec in the smart contract.
41
42
````solidity
43
// SPDX-License-Identifier: MIT
44
pragma solidity ^0.8.20;
45
46
/// @title Counter
47
/// @notice A simple counter contract that allows incrementing and setting a number
48
/// @dev This contract maintains a single uint256 state variable that can be modified
49
contract Counter {
50
    /// @notice The current counter value
51
    /// @dev Public state variable automatically generates a getter function
52
    uint256 public number;
53
54
    /// @notice Sets the counter to a specific value
55
    /// @dev Updates the number state variable to the provided value
56
    /// @param newNumber The new value to set the counter to \n
57
    /// \n
58
    /// ```markdown-ui-widget \n
59
    /// { "type": "form", "id": "setNumber", "submitLabel": "Set Number", "fields": [{ "type": "text-input", "id": "newValue", "label": "New Counter Value", "placeholder": "Enter number", "default": "42" }] } \n
60
    /// ``` \n
61
    function setNumber(uint256 newNumber) public {
62
        number = newNumber;
63
    }
64
65
    /// @notice Increments the counter by 1
66
    /// @dev Increases the number state variable by 1 using the increment operator \n
67
    /// \n
68
    /// ```markdown-ui-widget \n
69
    /// { "type": "form", "id": "increment", "submitLabel": "Increment", "fields": [] } \n
70
    /// ```\n
71
    function increment() public {
72
        number++;
73
    }
74
}
75
````
76
77
You might have noticed our one small twist: the Markdown UI component.
78
79
```markdown
80
`markdown-ui-widget
81
{ "type": "form", "id": "increment", "submitLabel": "Increment", "fields": [] }
82
`
83
```
84
85
This is what we can use in our front end to build interactive components along side the markdown describing how it works! When we compile this contract it's going to include a json file with out generated `userdoc` and `devdoc`. In order to make it easier to share these files along with the ABI, we can verify the contract with [Sourcify](https://sourcify.dev/) which will store our contract metadata for anyone to fetch via an API. That API response looks something like this when we use the query `?fields=devdoc`:
86
87
````json
88
{
89
  "devdoc": {
90
    "kind": "dev",
91
    "title": "Counter",
92
    "details": "This contract maintains a single uint256 state variable that can be modified",
93
    "methods": {
94
      "increment()": {
95
        "details": "Increases the number state variable by 1 using the increment operator \\n \\n ```markdown-ui-widget \\n { \"type\": \"form\", \"id\": \"increment\", \"submitLabel\": \"Increment\", \"fields\": [] } \\n ```\\n"
96
      },
97
      "setNumber(uint256)": {
98
        "params": {
99
          "newNumber": "The new value to set the counter to \\n \\n  ```markdown-ui-widget \\n { \"type\": \"form\", \"id\": \"setNumber\", \"submitLabel\": \"Set Number\", \"fields\": [{ \"type\": \"text-input\", \"id\": \"newValue\", \"label\": \"New Counter Value\", \"placeholder\": \"Enter number\", \"default\": \"42\" }] } \\n ``` \\n"
100
        },
101
        "details": "Updates the number state variable to the provided value"
102
      }
103
    },
104
    "version": 1,
105
    "stateVariables": {
106
      "number": {
107
        "details": "Public state variable automatically generates a getter function"
108
      }
109
    }
110
  },
111
  "matchId": "8931344",
112
  "creationMatch": "exact_match",
113
  "runtimeMatch": "exact_match",
114
  "verifiedAt": "2025-08-31T16:03:47Z",
115
  "match": "exact_match",
116
  "chainId": "11155111",
117
  "address": "0xEeF9B4a84C3327860CD14E1E066D7D6762b9bC3F"
118
}
119
````
120
121
As you can see we're able to get all of the markdown we put in earlier. Now all we have to do is create a frontend client that can render it all!
122
123
```typescript
124
import { useState, useEffect } from "react";
125
import { MarkdownUI } from "@markdown-ui/react";
126
import { Marked } from "marked";
127
import { markedUiExtension } from "@markdown-ui/marked-ext";
128
import "@markdown-ui/react/widgets.css";
129
import {
130
  parseContractToMarkdown,
131
  type ContractResponse,
132
} from "./utils/contractParser";
133
134
const marked = new Marked().use(markedUiExtension);
135
136
const CONTRACT_ADDRESS = "0xEeF9B4a84C3327860CD14E1E066D7D6762b9bC3F";
137
const CHAIN_ID = "11155111"; // Sepolia
138
139
function App() {
140
  const [contractHtml, setContractHtml] = useState<string>("");
141
  const [loading, setLoading] = useState<boolean>(true);
142
  const [error, setError] = useState<string | null>(null);
143
144
  useEffect(() => {
145
    const fetchContractData = async () => {
146
      try {
147
        setLoading(true);
148
        const response = await fetch(
149
          `https://sourcify.dev/server/v2/contract/${CHAIN_ID}/${CONTRACT_ADDRESS}?fields=devdoc`
150
        );
151
152
        if (!response.ok) {
153
          throw new Error(`HTTP error! status: ${response.status}`);
154
        }
155
156
        const data: ContractResponse = await response.json();
157
158
        const markdownContent = parseContractToMarkdown(data);
159
160
        console.log(markdownContent);
161
        const html = await marked.parse(
162
          markdownContent || "# No markdown widgets found"
163
        );
164
        setContractHtml(html);
165
      } catch (err) {
166
        console.error("Error fetching contract data:", err);
167
        setError(err instanceof Error ? err.message : "Unknown error");
168
      } finally {
169
        setLoading(false);
170
      }
171
    };
172
173
    fetchContractData();
174
  }, []);
175
176
  if (loading) {
177
    return (
178
      <div className="mx-auto flex min-h-screen max-w-xl flex-col items-center justify-center gap-6">
179
        <p>Loading contract data...</p>
180
      </div>
181
    );
182
  }
183
184
  if (error) {
185
    return (
186
      <div className="mx-auto flex min-h-screen max-w-xl flex-col items-center justify-center gap-6">
187
        <p className="text-red-500">Error: {error}</p>
188
      </div>
189
    );
190
  }
191
192
  return (
193
    <div className="mx-auto flex min-h-screen max-w-xl flex-col items-center justify-center gap-6">
194
      <MarkdownUI html={contractHtml} />
195
    </div>
196
  );
197
}
198
199
export default App;
200
```
201
202
As a result we get a nice page that not only has markdown formatting but interactive UI components that are built in thanks to Markdown UI.
203
204
![natspec demo](../../assets/blog-images/natspec-markdown-ui-2.png)
205
206
I took a little extra time to add in Wagmi to the app which resulted in a fully interactive contract, which you can check out [here](https://natspec-ui.orbiter.website). In a sense we achievied the goal of a unified standard markup that can make user interactions with contracts easier. Of course we have to keep in mind the limitations here, primarily being it would require developers to make sure they include all of this markup in their contract and the Markdown UI standard isn't even out of a beta stage, and for that reason I would highly recommend a professional solution like the Contracts UI Builder. Nevertheless it's fun to see how extensible and open Markdown and Solidity have come in the past few years. Each day we're getting closer to an internet that is not only safe, but user friendly as well.
207
208
As always the code for this small project is open source and can be found [here](https://github.com/stevedylandev/natspec-markdown-ui)!