TL;DR
- Three or more hyphens on their own line —
---— is a markdown horizontal line. Asterisks (***) and underscores (___) work too. - You must leave a blank line above
---, or the parser turns it into a setext H2 heading. - In MDX,
---at the top of a file is YAML frontmatter, not a horizontal rule. Use<hr />if in doubt. - Most horizontal rules in docs are a smell — a missing heading, a callout that wasn't written, or a paragraph break that needed whitespace.
A markdown horizontal line is three or more hyphens (---), asterisks (***), or underscores (___) on a line by themselves, surrounded by blank lines. Every standard parser compiles it to a single <hr> HTML element. That's the whole feature — and also where the trouble starts.
This guide covers the three syntaxes, the one rule that breaks them, and the rendering edge cases in GitHub, GitLab, and MDX that cost developers twenty minutes apiece. Then we'll argue you should probably use fewer of them.
Prefer to watch first? Web Dev Simplified's crash course walks through the full markdown syntax — horizontal rules, headings, lists, and the rest — in one sitting:
The three ways to make a horizontal line in markdown#
The CommonMark spec for thematic breaks calls them "thematic breaks." It accepts three characters, in three-or-more counts, on a line by themselves:
---
***
___All three render to the exact same HTML:
<hr />You can use more than three if you want. These are all valid:
----
**********
___________You can also put spaces between the characters:
- - -
* * *That's it for the syntax. Three flavors, identical output. The MDN reference for the <hr> element covers what the browser does with it: by default, a thin gray line with vertical margin.
Use hyphens
Most style guides recommend --- because it's the most readable in raw markdown. Asterisks are easy to confuse with bold or list bullets. Underscores look like an em-dash to the human eye. Pick hyphens and move on.
What about <hr> directly?#
Yes, you can write raw HTML:
Some paragraph text.
<hr />
Another paragraph.This works in GitHub Flavored Markdown, MDX, and almost every parser you'll touch. It's the right choice when you need a class or inline style:
<hr class="my-divider" style="border-color: #6366f1;" />The one rule that trips everyone up: blank lines#
Here is the bug that ships in roughly every README on the planet:
About this project
---
This project does the thing.You wrote that expecting a horizontal line between two paragraphs. You got an H2 heading that says "About this project" and a paragraph that says "This project does the thing." The <hr> never rendered.
That's because of setext headings — an alternate heading syntax where you underline text with === (for H1) or --- (for H2). When the markdown parser sees --- directly under a line of text, the heading rule wins.
The fix is one blank line:
About this project
---
This project does the thing.Now --- is on its own with whitespace above and below it, and the parser treats it as a thematic break.
This is the #1 markdown horizontal line bug
If your --- isn't rendering as an hr, ninety percent of the time it's because the line above has no blank line. The parser silently turned it into a heading. Always leave a blank line above and below ---.
The same trap applies to === (which makes H1 instead of H2). And no, this doesn't happen with *** or ___ — those don't have a setext meaning. If you're writing a lot of dividers inside flowing prose, *** is genuinely safer. But it looks worse, so most people just remember the blank-line rule.
How horizontal lines render across platforms#
The syntax is the same. The visual output isn't. Here's how the markdown horizontal line behaves in the places you'll actually publish it.
GitHub READMEs#
GitHub Flavored Markdown follows the GitHub Flavored Markdown spec, which extends CommonMark. The horizontal rule renders as a 1px line with about 24px of vertical margin. Wide, full-width, hard to miss.
GitHub's renderer is forgiving — it accepts all three syntaxes, with or without spaces, three characters or more. If you're writing a great GitHub README, --- works exactly how you'd expect.
GitLab and Bitbucket#
Both follow CommonMark with minor extensions. The behavior is identical to GitHub for thematic breaks. The visual styling is slightly different — GitLab's <hr> is a touch lighter, Bitbucket's is the heaviest of the three — but nothing breaks.
CommonMark previewers (VS Code, etc.)#
These follow the spec literally. If your --- renders correctly in GitHub but not in your local preview, you almost certainly have a missing blank line that GitHub's renderer is being lenient about. Trust the strict parser; fix the source.
MDX-based docs platforms#
MDX is where it gets interesting, because MDX is a superset of markdown that also parses JSX. That means <hr /> works the same as in HTML — and --- mostly works, except at the top of a file and inside JSX blocks. The next section covers this.
Horizontal lines in MDX (and why they sometimes break)#
MDX lets you mix markdown with React components. That power costs you two specific edge cases for the markdown horizontal line.
Gotcha 1: --- at the top of an MDX file is frontmatter#
---
title: My Page
---
# WelcomeThose first three hyphens aren't a horizontal rule. They're the opening of a YAML frontmatter block. The MDX compiler reads everything between the two --- lines as metadata for the page.
If you actually want a horizontal line as the very first element on a page (you probably don't), you need to use <hr /> instead:
<hr />
# WelcomeGotcha 2: --- inside a JSX block doesn't render as an hr#
MDX parses content inside JSX components differently. This won't work the way you expect:
<div>
Some text.
---
More text.
</div>Inside the <div>, MDX treats the content as JSX children. The --- becomes a literal string "---", not a thematic break. The fix is either to move the content outside the JSX wrapper, or to use <hr /> explicitly:
<div>
Some text.
<hr />
More text.
</div>When in doubt, use the HTML tag
<hr /> works everywhere --- works, plus all the places --- doesn't — inside JSX, at the top of a file, inside tables, inside callouts. If you're writing MDX for documentation, make <hr /> your default and save --- for plain README files.
Gotcha 3: linters that auto-format hyphens#
Prettier and some MDX linters will normalize *** and ___ to ---. That's usually fine, but if you wrote *** specifically because it's adjacent to text without a blank line, the auto-fix can introduce the setext heading bug. Check your diff after running format-on-save.
Styling the default <hr>: when plain isn't enough#
The browser's default <hr> is a thin gray line with no personality. In a serious docs site, it looks like a forgotten dividing rule from 1999. Three ways to make it better.
Option 1: CSS#
Target hr in your stylesheet:
hr {
border: none;
height: 1px;
background: linear-gradient(to right, transparent, #e5e5e5, transparent);
margin: 2.5rem 0;
}That fades the line to transparent at the edges — a small change that makes the divider feel intentional rather than utilitarian. The principles of modern documentation design lean hard on details like this.
Option 2: Raw HTML with classes#
If you only want some dividers styled (say, between sections vs. between examples), give them classes:
<hr class="section-divider" />.section-divider {
border: none;
height: 2px;
background: #6366f1;
width: 40px;
margin: 3rem auto;
}A 40px wide accent-colored bar centered between sections looks dramatically better than a full-width gray line, and it took six lines of CSS.
Option 3: Replace <hr> with a custom component#
In MDX, you can override what <hr> renders as. You feed the MDX provider a component map, and every --- and <hr /> in your content uses your component instead. That's how Dokly's MDX component reference handles dividers — same authoring experience, much better output.
const components = {
hr: () => (
<div className="my-8 flex items-center justify-center">
<span className="text-zinc-400">◆</span>
</div>
),
};Now every horizontal line in your docs is a centered diamond. Or three dots. Or whatever fits your brand. The markdown source stays clean.
When NOT to use a horizontal line#
Here's the part most posts skip. The markdown horizontal line is the third-most-overused element in technical writing, behind bold text and exclamation marks. Most uses fall into one of these categories:
The "I needed a break here" hr. You're transitioning between two ideas inside one section, so you drop in a divider. The honest fix is usually a new H3. Headings give readers a place to jump to. Horizontal lines don't.
The "this section is over" hr. If the next H2 doesn't make it obvious that the previous section ended, the section itself is too long. Split it.
The "decorative" hr at the end of the page. Browsers already render generous whitespace at the end of the document. Adding a horizontal line there just adds visual noise.
The "between every callout" hr. Callouts already have borders and backgrounds. They don't need dividers above or below them.
Our rule of thumb
A docs page that's working well has zero or one horizontal rules. If you're reaching for a third one, the page wants to be two pages, or it wants a better heading structure.
Better alternatives for visual separation in docs#
When you feel the urge to drop in a ---, try one of these first.
A new heading#
If two paragraphs belong to different ideas, they want different headings. The H2/H3 boundary is the natural divider. The reader gets an anchor link. The page gets a table-of-contents entry. Everybody wins.
A callout#
A <Callout> does what a horizontal rule pretends to do: it visually separates a chunk of content from the surrounding flow. Plus it carries meaning (info, tip, warning, success) that a plain line can't.
<Callout type="tip">
Use `Cmd+K` to jump to any page in your docs.
</Callout>A card group#
For "here are three related things" or "here's where to go next," cards beat a divider followed by a bulleted list every time.
MDX component reference
The components Dokly renders out of the box
MDX vs plain markdown
When the upgrade is worth it
Just whitespace#
Browsers give paragraphs ~1em of margin by default. That's already a visual break. Adding a horizontal rule on top says "this gap is really important" — which is rarely true. Trust the whitespace.
Quick recap: the working workflow#
Decide if you actually need a divider
Most of the time you want a heading, a callout, or whitespace. If you still want a divider, continue.
Write `---` on its own line
Three hyphens. Not asterisks, not underscores. Hyphens read best in source.
Leave a blank line above and below
This is the rule that prevents the setext H2 bug. Always.
In MDX, prefer `<hr />` for safety
Especially inside JSX, near the top of a file, or anywhere you've had a renderer surprise you.
Style it once in CSS
A faded gradient or a centered accent bar. Six lines of CSS, applied everywhere, done.
Frequently Asked Questions#
What is the simplest way to make a horizontal line in markdown?#
Three hyphens on their own line: ---. Make sure there's a blank line above and below it. That renders to an <hr> tag in every standard markdown parser, including GitHub, GitLab, CommonMark, and MDX.
Why does --- create a heading instead of a horizontal line?#
If you put --- immediately under a line of text with no blank line between them, markdown parses it as a setext-style H2 heading, not a horizontal rule. The fix is simple: leave a blank line between the text and the ---. This is the single most common markdown horizontal line bug.
Do *** and ___ work the same as --- for horizontal lines?#
Yes. CommonMark accepts three or more hyphens, asterisks, or underscores on a line by themselves. All three render to the same <hr> HTML element. Hyphens are the most readable, which is why most style guides recommend them.
Can I use <hr> directly in markdown?#
Yes. Most markdown parsers, including GitHub Flavored Markdown and MDX, accept raw HTML. Writing <hr /> works identically to --- and is the right choice if you want to add a class or inline style for custom rendering.
Why doesn't my horizontal line show up in MDX?#
Two common causes. First, --- at the very top of an MDX file is interpreted as the start of YAML frontmatter, not a horizontal rule. Second, if your MDX is inside a JSX block, the parser may treat the line as part of that block. Move it outside the JSX or use <hr /> instead.
How do I style a horizontal line in markdown?#
Markdown itself has no styling. Either target hr in your stylesheet (hr { border: none; height: 1px; background: #e5e5e5; }) or write raw HTML like <hr class="my-divider" />. Some platforms also let you swap the default <hr> for a custom component.
What's the difference between a markdown line break and a horizontal line?#
A line break ends a line of text and moves the next words to the next line (two trailing spaces or a <br /> tag). A horizontal line is a visual divider between blocks of content (--- or <hr />). The first is inline, the second creates a new block element.
If you want to write markdown without thinking about which character renders as what, ship docs without touching Git using Dokly — the editor previews every divider, callout, and heading as you type, so the setext bug can't reach production. Try Dokly free.