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.jsonand one body file in the post’s folder:index.mdorindex.mdx, never both. - You don’t need
importlines. Every component on this page is already available in any.mdxarticle. - Components start with a capital letter and look like HTML tags:
<Callout>…</Callout>, or<PaymentRails />when they have no content inside. - Run
npm run devand 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.
## A section
### A subsectionText, links and lists
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
You can write **bold**, _italic_, `inline code` and [links](https://example.com).
- Bulleted item
1. Numbered itemQuotes
Good software is built by teams that trust each other enough to disagree.
> Good software is built by teams that trust each other enough to disagree.Tables
| Format | Best for | Components |
|---|---|---|
index.md | Text-only posts, quick updates | No |
index.mdx | Deep-dives, launches, visual explanations | Yes |
| 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.
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).
public Money settle(Payment payment) {
return ledger.post(payment.debit(), payment.credit());
}```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”.
export const settle = (payment: Payment) =>
ledger.post(payment.debit, payment.credit);npm run build:staging```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.
SELECT account_id,
SUM(amount) AS balance
FROM ledger_entries
WHERE posted_at < :cutoff
GROUP BY account_id;```sql showLineNumbers
SELECT account_id, …
```Highlighting lines
List line numbers in braces after the language. Ranges work too: {1,4-6}.
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();
};```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.
const limit = pLimit(8);
const results = await Promise.all(jobs.map((job) => limit(job))); ```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.
await payments.create({ amount, currency, idempotencyKey });
await audit.log('payment.created', { idempotencyKey });```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.
const client = new HttpClient({
timeout: 30_000,
timeout: 5_000,
retries: 3,
});```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.
@Service
class LookupService(private val repo: LookupRepository) {
fun byCode(code: String): Lookup =
repo.findByCode(code) ?: throw LookupNotFound(code)
}```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.
<Callout title='Tip' tone='tip'>
For practical advice the reader can act on.
</Callout>| Prop | Values | Default |
|---|---|---|
title | Any short text | No 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:
<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.
<Figure caption='How an engagement grows, from review to evaluation'>
<TrustLadder />
</Figure>| Prop | Values | Default |
|---|---|---|
caption | Any short text | No 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
<dependency>
<groupId>com.pratishthan</groupId>
<artifactId>ledger-client</artifactId>
<version>2.4.0</version>
</dependency>Gradle
implementation("com.pratishthan:ledger-client:2.4.0")Maven
mvn -q testGradle
./gradlew test<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>| Component | Prop | Values | Default |
|---|---|---|---|
Tabs | group | Any short name | None: switches on its own |
Tab | label | Any short text | Required |
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.
-
Clone the repository
Terminalgit clone git@github.com:Pratishthan/ledger-service.git -
Start the local database
Terminaldocker compose up -d postgres -
Run the service
Open
http://localhost:8080/healthto check it’s up.
<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/
- main/
- docker-compose.ymllocal Postgres
- pom.xml
- src/
<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.
/lms/api/v1/lookups/code/{lookupCode}Returns one lookup table by its code. Responds with 404 when the code
doesn’t exist.
/lms/api/v1/lookups/{id}/modification<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' />| Prop | Values |
|---|---|
method | GET, POST, PUT, PATCH or DELETE |
path | The 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.
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)<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 />One payment, five eras. A single payment moves from paper cheque to today’s rails. Good for a product or industry-history post.
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 />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 />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 />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.
Sample amounts. Illustrates the review flow only.
<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.
An illustration of the idea, not test data.
<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 />Capability rings. A bead laps each ring in turn, one ring per tier of the capability model. Good for product and platform posts.
<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 />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 />Platform diagrams
Platform stack. The capability model as an isometric stack: credential, arena, engine, AI multiplier.
<PlatformStack />Payment rails. ACH, SWIFT, BACS, FPS and CHAPS converging on one payment hub.
<PaymentRails />Scale grid. 100M accounts, 3,000 TPS and under 2 ms response, shown as a wave of activity across an account grid.
<ScaleGrid />Trust ladder. Review, proof of concept, production and evaluation as stairs that light up in turn. It appears in the Figure section above.
<TrustLadder />Picking components by topic
| Article type | Components that usually fit |
|---|---|
| Tech | Code blocks with titles and diffs, Tabs, Steps, FileTree, Endpoint, ThroughputHold, ScaleGrid |
| Product | PlatformStack, CapabilityRings, PaymentRails, PaymentEras, tip callouts |
| Culture | Quotes, 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 of | Write |
|---|---|
<!-- comment --> | {/* comment */} |
{ or } in a sentence | Put it in backticks, or escape it as \{ and \} |
< in a sentence, e.g. < 2 ms | “under 2 ms”, or < |
<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:
- Build the component (
.astrois simplest) insrc/components/. - Add it to the list in
mdxComponents.ts. - Add a section to this article showing it live, so the next author can find it.