Skip to content
All articles

Writing with MDX: every component you can use in an article

A working reference for authors: Markdown basics, code blocks, tech article components such as tabs and file trees, and all fourteen animated visuals, each shown live with the code to copy.

Most of our articles are plain text, and plain index.md is still the easiest way to write them. When a post needs more than text, such as a diagram of a payment flow in a tech deep-dive, a highlighted takeaway in a product update, or a pull quote in a culture story, name the file index.mdx instead. You can then drop components between paragraphs.

This article is written in MDX itself. Every component below is shown live, followed by the code that produced it.

Before you start

  • Put meta.json and one body file in the post’s folder: index.md or index.mdx, never both.
  • You don’t need import lines. Every component on this page is already available in any .mdx article.
  • Components start with a capital letter and look like HTML tags: <Callout>…</Callout>, or <PaymentRails /> when they have no content inside.
  • Run npm run dev and open your post while writing. If something is wrong, the build error names the file and line.

Markdown basics

Everything that works in index.md works in index.mdx too.

Headings

Use ## for sections and ### for subsections. Both appear in the table of contents beside the article. Don’t use #, because the post title is already the page’s main heading.

md
## A section

### A subsection

You can write bold, italic, inline code and links. Links to other sites open in a new tab automatically.

  • Bulleted lists use -
  • Numbered lists use 1.
    • Indent two spaces to nest
md
You can write **bold**, _italic_, `inline code` and [links](https://example.com).

- Bulleted item

1. Numbered item

Quotes

Good software is built by teams that trust each other enough to disagree.

md
> Good software is built by teams that trust each other enough to disagree.

Tables

FormatBest forComponents
index.mdText-only posts, quick updatesNo
index.mdxDeep-dives, launches, visual explanationsYes
md
| Format     | Best for                       |
| ---------- | ------------------------------ |
| `index.md` | Text-only posts, quick updates |

Code blocks

Fenced code blocks work as in any Markdown file. They get a header, a copy button and line highlighting too; see Code blocks below.

Images

Keep images in the post’s folder and refer to them with ./. The site resizes and converts them to WebP when it builds.

md
![Architecture of the settlement service](./settlement-architecture.png)

Code blocks

Every fenced code block, in .md and .mdx alike, gets a header and a Copy button. Name the language after the opening fence to get syntax highlighting (ts, java, kotlin, sql, bash, json, yaml and so on).

java
public Money settle(Payment payment) {
    return ledger.post(payment.debit(), payment.credit());
}
md
```java
public Money settle(Payment payment) { … }
```

File name

Add title="…" after the language to show a file name in the header. Shell blocks (bash, sh, shell) without a title are labelled “Terminal”.

src/payments/settle.ts
export const settle = (payment: Payment) =>
  ledger.post(payment.debit, payment.credit);
Terminal
npm run build:staging
md
```ts title="src/payments/settle.ts"
export const settle = …
```

```bash
npm run build:staging
```

Line numbers

Add showLineNumbers for long listings that the text refers to by line.

sql
SELECT account_id,
       SUM(amount) AS balance
FROM ledger_entries
WHERE posted_at < :cutoff
GROUP BY account_id;
md
```sql showLineNumbers
SELECT account_id, …
```

Highlighting lines

List line numbers in braces after the language. Ranges work too: {1,4-6}.

ts
export const retry = async <T>(task: () => Promise<T>, attempts = 3) => {
  for (let i = 1; i < attempts; i++) {
    try {
      return await task();
    } catch {
      await sleep(2 ** i * 100);
    }
  }
  return task();
};
md
```ts {2-3}
export const retry = …
```

To highlight a line without counting, end it with a [!code highlight] comment instead. The comment is removed from the output.

ts
const limit = pLimit(8);
const results = await Promise.all(jobs.map((job) => limit(job))); 
md
```ts
const results = await Promise.all(jobs.map((job) => limit(job))); // [!code highlight]
```

Highlighting words

Put a word between slashes after the language to highlight every place it appears.

ts
await payments.create({ amount, currency, idempotencyKey });
await audit.log('payment.created', { idempotencyKey });
md
```ts /idempotencyKey/
await payments.create({ amount, currency, idempotencyKey });
```

Showing a change (diff)

Mark added lines with [!code ++] and removed lines with [!code --]. Readers see the before and after in one block, and Copy copies only the new version.

ts
const client = new HttpClient({
  timeout: 30_000, 
  timeout: 5_000, 
  retries: 3, 
});
md
```ts
const client = new HttpClient({
  timeout: 30_000, // [!code --]
  timeout: 5_000, // [!code ++]
});
```

Focusing on lines

Mark the lines that matter with [!code focus]. The rest of the block is dimmed until the reader hovers over it.

kotlin
@Service
class LookupService(private val repo: LookupRepository) {
    fun byCode(code: String): Lookup =
        repo.findByCode(code) ?: throw LookupNotFound(code) 
}
md
```kotlin
repo.findByCode(code) ?: throw LookupNotFound(code) // [!code focus]
```

Callout

A highlighted box for something the reader shouldn’t miss: a key takeaway, a caveat, or a tip. It takes an optional title and a tone.

mdx
<Callout title='Tip' tone='tip'>
  For practical advice the reader can act on.
</Callout>
PropValuesDefault
titleAny short textNo title
tone"note", "tip" or "warning""note"

You can use Markdown inside a callout (bold, links, lists), as long as you leave a blank line after the opening tag and before the closing one:

mdx
<Callout title="Checklist before publishing">

- Real author and date in `meta.json`
- `"draft": false`

</Callout>

Figure

Wraps a visual (or an image) and adds a caption underneath. It also adds space above and below so the visual doesn’t crowd the text around it.

Engagement ladder: review, proof of concept, production system, evaluationLow commitmentHigh value01ReviewShould we even do this?02PoCWill it work on our data?03ProductionBuild it to survive.04EvaluationCan we trust it?
Every caption should say what the reader is looking at, not just name it
mdx
<Figure caption='How an engagement grows, from review to evaluation'>
  <TrustLadder />
</Figure>
PropValuesDefault
captionAny short textNo caption

A visual works without Figure too. Use Figure whenever a caption would help.

Tech article components

Tabs

Show the same example several ways, such as different languages, build tools or operating systems. Give tabs the same group and they switch together across the article; the reader’s choice is remembered on their next visit.

Maven

pom.xml
<dependency>
  <groupId>com.pratishthan</groupId>
  <artifactId>ledger-client</artifactId>
  <version>2.4.0</version>
</dependency>

Gradle

build.gradle.kts
implementation("com.pratishthan:ledger-client:2.4.0")

Maven

Terminal
mvn -q test

Gradle

Terminal
./gradlew test
mdx
<Tabs group="build">
  <Tab label="Maven">

```xml title="pom.xml"
<dependency>…</dependency>
```

  </Tab>
  <Tab label="Gradle">

```kotlin title="build.gradle.kts"
implementation("com.pratishthan:ledger-client:2.4.0")
```

  </Tab>
</Tabs>
ComponentPropValuesDefault
TabsgroupAny short nameNone: switches on its own
TablabelAny short textRequired

Leave a blank line after <Tab …> and before </Tab> so the code block inside is read as Markdown.

Steps

Turns a numbered list into a step-by-step guide, for setup instructions and tutorials. The first line of each item becomes the step’s title.

  1. Clone the repository

    Terminal
    git clone git@github.com:Pratishthan/ledger-service.git
  2. Start the local database

    Terminal
    docker compose up -d postgres
  3. Run the service

    Open http://localhost:8080/health to check it’s up.

mdx
<Steps>

1. Clone the repository

   ```bash
   git clone git@github.com:Pratishthan/ledger-service.git
   ```

2. Start the local database

</Steps>

Indent everything that belongs to a step (code, paragraphs) by three spaces so it stays inside that step.

File tree

Shows a folder structure. Write it as a nested bulleted list: names ending in / are folders, text after the name is a comment, and bold marks the files the article is about.

  • ledger-service/
    • src/
      • main/
        • LookupController.javaREST endpoints
        • LookupService.java
      • test/
    • docker-compose.ymllocal Postgres
    • pom.xml
mdx
<FileTree>

- ledger-service/
  - src/
    - **LookupController.java** REST endpoints
  - pom.xml

</FileTree>

Endpoint

Documents one API endpoint with a colour-coded method. The description underneath is optional and can contain Markdown.

GET/lms/api/v1/lookups/code/{lookupCode}

Returns one lookup table by its code. Responds with 404 when the code doesn’t exist.

PUT/lms/api/v1/lookups/{id}/modification
mdx
<Endpoint method='GET' path='/lms/api/v1/lookups/code/{lookupCode}'>
  Returns one lookup table by its code.
</Endpoint>

<Endpoint method='PUT' path='/lms/api/v1/lookups/{id}/modification' />
PropValues
methodGET, POST, PUT, PATCH or DELETE
pathThe URL path. Braces are fine inside quotes.

Keyboard keys

Use the standard <kbd> tag for shortcuts: press Ctrl + Shift + P to open the command palette.

mdx
Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd> to open the command palette.

Collapsible sections

Use the standard <details> tag for long output, full stack traces or optional background that most readers can skip.

Full stack trace
LookupNotFound: lookup "CURRENCY_X" not found
    at LookupService.byCode(LookupService.kt:4)
    at LookupController.get(LookupController.java:31)
mdx
<details>
<summary>Full stack trace</summary>

```text
LookupNotFound: lookup "CURRENCY_X" not found
```

</details>

Animated visuals

These are the same animated diagrams used on the marketing pages. They follow the light and dark theme, resize to the article’s width, and stop animating for readers who have turned on “reduce motion”. None of them take props: write the tag and you’re done.

Some visuals have a pause button or respond to clicks and holds. Readers can interact with them inside the article just as they can on the marketing pages.

Banking objects

Passbook printer. A dot-matrix printer stamps our proof points line by line. Good for a company milestone or culture post.

PassbookPrinter
mdx
<PassbookPrinter />

One payment, five eras. A single payment moves from paper cheque to today’s rails. Good for a product or industry-history post.

Example BankDate 06 11 2026
Pay Meera Iyer
Rupees ₹12,500.00 only
Cheque no. 004217Signed Asha Rao
ZCZC 0417
TO   EXAMPLE BANK BENGALURU
TEST 48213
REF  CHQ004217
VAL  06 NOV 26
PAY  INR 12500.00
BY   ORDER OF ASHA RAO
TO   MEERA IYER
NNNN
:20:CHQ004217
:23B:CRED
:32A:261106INR12500,00
:50K:/0123456789
ASHA RAO
:59:/9876543210
MEERA IYER
:71A:SHA
<EndToEndId>CHQ004217</EndToEndId>
<IntrBkSttlmAmt Ccy="INR">12500.00</IntrBkSttlmAmt>
<IntrBkSttlmDt>2026-11-06</IntrBkSttlmDt>
<Dbtr><Nm>Asha Rao</Nm></Dbtr>
<Cdtr><Nm>Meera Iyer</Nm></Cdtr>
{
  "reference": "CHQ004217",
  "valueDate": "2026-11-06",
  "amount": 12500.00, "currency": "INR",
  "debtor": "Asha Rao",
  "creditor": "Meera Iyer",
  "initiatedBy": "agent",
  "approval": "required"
}

Written by hand, carried by courier, cleared by people. Every fact the bank needs is already here.

Sample payment with made-up names and account numbers.

PaymentEras
mdx
<PaymentEras />

Four zooms to one account. Readers click through nested grids from 100,000,000 accounts down to one. Good for posts about scale or data modelling.

100,000,000

accounts in view

•• •• •• ••

This shows the scale, not how the cache works. The Global Cache Platform answers in under 2 ms.

AccountZoom
mdx
<AccountZoom />

Hold to feel 3,000 a second. Readers press and hold while a 3,000-cell grid fills in exactly one second. Good for performance engineering posts.

A blink takes about 300 ms. At this rate that is 900 transactions, each answered in under 2 ms.

ThroughputHold
mdx
<ThroughputHold />

Reconciliation zip. Two records close like a zip, and the one entry that doesn’t match is flagged by AI and decided by a person. Good for AI and operations posts.

StartClosed

Sample amounts. Illustrates the review flow only.

ReconciliationZip
mdx
<ReconciliationZip />

Grade test. Banks, peak days and new rules pile onto two beams; one breaks and the other earns the product-grade stamp. Good for posts on engineering quality.

Year oneYears later

An illustration of the idea, not test data.

GradeTest
mdx
<GradeTest />

Logo rings

Rings swarm. Thousands of ticks travel along the foundation line and climb up to form the logo rings. Good as an opener for company or culture posts.

RingsSwarm
mdx
<RingsSwarm />

Capability rings. A bead laps each ring in turn, one ring per tier of the capability model. Good for product and platform posts.

CapabilityRings
mdx
<CapabilityRings />

Five rails, one foundation. Five payment rails file into the rings, lap them and leave along the foundation line. Good for payments product posts.

RailsTrail
mdx
<RailsTrail />

Project grade vs. product grade. A ring abandoned bank after bank, beside one that ships to many. Good for posts on how we build.

IT services staff projects

They learn banking on your budget, then move on to the next engagement.

We engineer products

Platforms that ship to many banks, scale globally and survive years of regulatory change.

GradeContrast
mdx
<GradeContrast />

Platform diagrams

Platform stack. The capability model as an isometric stack: credential, arena, engine, AI multiplier.

Capability model: credential, arena, engine and AI multiplier stacked as four layersThe multiplierAI inside the core, done safelyThe engineArchitecture · quality · performanceThe arenaCore banking · payments · collectionsThe credentialBuilt a leading banking platform
PlatformStack
mdx
<PlatformStack />

Payment rails. ACH, SWIFT, BACS, FPS and CHAPS converging on one payment hub.

ACH, SWIFT, BACS, FPS and CHAPS payment rails connected through one payment hubACHSWIFTBACSFPSCHAPSPaymenthubBankingplatform
PaymentRails
mdx
<PaymentRails />

Scale grid. 100M accounts, 3,000 TPS and under 2 ms response, shown as a wave of activity across an account grid.

Global Cache Platform: 100 million accounts, 3,000 transactions per second, under 2 milliseconds response100Maccounts3,000transactions / sec< 2 msresponse time
ScaleGrid
mdx
<ScaleGrid />

Trust ladder. Review, proof of concept, production and evaluation as stairs that light up in turn. It appears in the Figure section above.

mdx
<TrustLadder />

Picking components by topic

Article typeComponents that usually fit
TechCode blocks with titles and diffs, Tabs, Steps, FileTree, Endpoint, ThroughputHold, ScaleGrid
ProductPlatformStack, CapabilityRings, PaymentRails, PaymentEras, tip callouts
CultureQuotes, RingsSwarm, PassbookPrinter, GradeContrast, note callouts

One or two visuals per article is usually enough. Each one is animated, so a page full of them competes with the writing.

MDX syntax to watch out for

MDX is stricter than Markdown. Outside code blocks and backticks, a few characters mean “this is code”:

Instead ofWrite
<!-- comment -->{/* comment */}
{ or } in a sentencePut it in backticks, or escape it as \{ and \}
< in a sentence, e.g. < 2 ms“under 2 ms”, or &lt;
<br><br /> (tags must be closed)

If the build fails, the error message names the file and line. Fix that line and save; the dev server reloads.

Adding a new component

Authors can only use components listed in src/components/mdx/mdxComponents.ts. To add one:

  1. Build the component (.astro is simplest) in src/components/.
  2. Add it to the list in mdxComponents.ts.
  3. Add a section to this article showing it live, so the next author can find it.