---
title: "MDX components"
description: "Use your own Rails partials inside Markdown."
url: "https://www.andromedacms.dev/docs/mdx-components"
updated_at: 2026-09-24
---

# MDX components

import Callout from 'content_components/callout';

An `.mdx` file is Markdown that can also call components. In Andromeda a
component is a plain Rails partial — there is no component framework to adopt.

<Callout type="tip" title="Rails all the way down">
  Partials keep the door open: a component can use helpers, presenters and
  anything else your application already has.
</Callout>

## Calling a component

```mdx
import Callout from 'content_components/callout';

<Callout type="warning" title="Careful">
  The body of a component is **Markdown** too.
</Callout>
```

## Writing one

```erb
<%# app/views/content_components/_callout.html.erb %>
<aside class="callout callout--<%= local_assigns.fetch(:type, "note") %>">
  <% if local_assigns[:title] %>
    <p class="callout__title"><%= title %></p>
  <% end %>
  <%= content %>
</aside>
```

- Props become locals. `camelCase` props arrive as `snake_case`.
- The children of the tag arrive as `content`, already rendered.
- `<Fragment slot="header">…</Fragment>` arrives as a `header` local.
- Optional props are read through `local_assigns`.

`bin/rails generate andromeda:component Callout type title` writes the stub.

## How a tag is resolved

1. An explicit registration, if you made one.
2. The path in the `import` statement, when it points at a partial.
3. The naming convention: `Callout` → `app/views/content_components/_callout.html.erb`.

A component with no partial is an error that names the file, the line and the
path to create — never a silently dropped tag.

## Expressions

Literals and `{frontmatter.title}` are evaluated; `{/* comments */}` are
dropped. Anything that would need a JavaScript runtime raises an error instead
of being ignored, so a page never renders half of what its author wrote.

## When components render

Components are expanded during the build, not per request. That makes a page
cheap to serve, and means a component cannot depend on the current request —
no `current_user` inside the body. Put per-user markup in the layout, or load
it after the fact with a lazy Turbo Frame.
