How to Use MDX Expressions in DESIGN.md Files with the `spec` Command

MDX expressions in spec.mdx files are evaluated by the npx @google/design.md spec command using a sandboxed compiler that replaces JavaScript expressions with their computed markdown output, generating the final docs/spec.md specification.

The google-labs-code/design.md repository provides a CLI tool that transforms MDX source files into human-readable specifications. When you run the spec command, it processes embedded MDX expressions in your spec.mdx file to dynamically generate content based on your configuration and reusable renderer functions.

Understanding the MDX Compilation Pipeline

The spec command triggers a three-stage pipeline that converts MDX syntax into static markdown. Understanding these stages helps you write effective expressions and debug compilation issues.

Entry Point and Scope Generation

In packages/cli/src/linter/spec-gen/generate.ts, the generation process begins by reading the spec.mdx source file and constructing a scope object. This scope binds two critical resources: the SPEC_CONFIG configuration data and a set of pure-function renderers imported from renderers.ts.

The generate.ts module orchestrates the workflow:

  1. Reads spec.mdx (referenced as MDX_PATH)
  2. Builds a scope containing SPEC_CONFIG and renderer functions
  3. Calls compileMdx(source, scope) to transform the MDX into plain markdown
  4. Writes the generated output to docs/spec.md

AST Parsing and Expression Evaluation

The packages/cli/src/linter/spec-gen/compiler.ts module handles the actual MDX processing. It uses the unified ecosystem with remark-parse and remark-remark-mdx to parse the source into an AST.

When the compiler encounters expression nodes, it evaluates them using a sandboxed Function constructor:

  • Inline expressions (mdxTextExpression nodes like {sectionOrderList()}) execute within the scope and return strings that replace the node directly
  • Block expressions (mdxFlowExpression nodes like {colorsExample()}) execute similarly but wrap results in HTML nodes to preserve markdown block formatting

Renderer Functions

The packages/cli/src/linter/spec-gen/renderers.ts file contains pure functions that generate reusable markdown fragments. Each renderer receives the fully typed SpecConfig object and returns a markdown string. These functions are exposed to the MDX scope, allowing you to call them directly within curly braces.

How MDX Expressions Are Evaluated

The compiler distinguishes between two expression types based on their position in the document:

Expression Type Node Type Output Handling Use Case
Inline mdxTextExpression Replaced with plain text node Dynamic values inside paragraphs
Block mdxFlowExpression Wrapped in HTML node preserving markdown Tables, code blocks, lists

Because the evaluator uses new Function(...scope), expressions can only reference variables explicitly provided in the scope. You cannot import arbitrary modules or access Node.js globals outside the defined scope.

Practical Examples

Inline Expressions for Dynamic Values

Use curly braces within text to inject configuration values or simple renderer output:


# DESIGN.md Format

The current spec version is **{SPEC_VERSION}**.

{sectionOrderList()}

When processed, {SPEC_VERSION} pulls from SPEC_CONFIG, while {sectionOrderList()} calls the renderer to generate a numbered list:


# DESIGN.md Format

The current spec version is **alpha**.

1. Overview
2. Colors
3. Typography
4. Layout
5. Elevation & Depth
6. Shapes
7. Components
8. Do's and Don'ts

Block-Level Expressions for Complex Content

For tables, code blocks, or multi-line content, use block expressions that occupy their own line:


## Colors

{colorsExample()}

The colorsExample() renderer returns a fenced code block with YAML front matter. The compiler preserves the markdown formatting, producing:


## Colors

```yaml
---
version: alpha
name: Daylight Prestige
colors:
  primary: "#1A1C1E"
  secondary: "#6C7278"
  tertiary: "#B8422E"
typography:
  h1:
    fontFamily: Public Sans
    fontSize: 48px
    fontWeight: 600
    lineHeight: 1.1
    letterSpacing: -0.02em
---

### Creating Custom Renderers

To add domain-specific content, extend the rendering system in three steps.

First, define the function in [`packages/cli/src/linter/spec-gen/renderers.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/spec-gen/renderers.ts):

```typescript
export function myCustomBanner(cfg: SpecConfig): string {
  return `> **NOTE:** This spec was generated on ${new Date().toDateString()} using version ${cfg.SPEC_VERSION}.`;
}

Second, expose it in packages/cli/src/linter/spec-gen/generate.ts by adding to the scope:

myCustomBanner: () => renderers.myCustomBanner(cfg),

Third, invoke it in spec.mdx:

{myCustomBanner()}

The compiled output will contain the dynamic banner with the current date and version string.

Running the spec Command

Execute the generation process from your project root:

npx @google/design.md spec

This command reads the default spec.mdx path, compiles the MDX expressions using the pipeline described above, and writes the final specification to docs/spec.md. The command also supports outputting to stdout or custom file paths as documented in the repository README under the spec heading.

Summary

  • MDX expressions in spec.mdx enable dynamic content generation by executing JavaScript within a sandboxed scope during the spec command execution.
  • The compilation pipeline in packages/cli/src/linter/spec-gen/ consists of generate.ts for orchestration, compiler.ts for AST processing, and renderers.ts for markdown generation functions.
  • Two expression types exist: inline expressions (mdxTextExpression) for text replacement and block expressions (mdxFlowExpression) for complex markdown structures.
  • Custom renderers extend functionality by adding pure functions to renderers.ts and exposing them in the scope within generate.ts.
  • The npx @google/design.md spec command processes these expressions to produce the final human-readable docs/spec.md specification.

Frequently Asked Questions

What is the difference between inline and block MDX expressions?

Inline expressions use mdxTextExpression nodes and work within paragraphs or headings, returning text that replaces the curly braces directly. Block expressions use mdxFlowExpression nodes and stand alone on their own lines, returning markdown that the compiler wraps in HTML nodes to preserve formatting. According to the source code in compiler.ts, both execute within the same sandboxed scope but differ in how the AST transformer handles their output.

Can I import external modules in MDX expressions?

No. The compiler in packages/cli/src/linter/spec-gen/compiler.ts uses a new Function(...scope) constructor that limits expression evaluation to the provided scope variables. You cannot use import, require, or reference Node.js globals. All data must come from SPEC_CONFIG or renderer functions exposed in the scope by generate.ts.

How do I add custom rendering logic?

Create a pure function in packages/cli/src/linter/spec-gen/renderers.ts that accepts the SpecConfig type and returns a markdown string. Then expose that function in the scope object within packages/cli/src/linter/spec-gen/generate.ts. Finally, call the function using curly braces in spec.mdx. The function will execute during compilation and its return value will appear in the generated specification.

Where does the spec command write its output?

By default, the spec command writes the compiled markdown to docs/spec.md in your project directory. This behavior is controlled by the generate.ts entry point, which reads from spec.mdx (referenced as MDX_PATH) and outputs the processed content. You can verify the output path and override options in the README documentation under the spec command section.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →