Skip to content

Styling Markdown

Markdown turns into ordinary HTML. Paragraphs and headings already pick up the styles from global.css, but articles also need spacing, readable tables, code blocks, quotes, footnotes and a few less common elements.

I scope these styles to .markdown-container.

<main class="base-layout base-layout-content">
<article class="markdown-container">
<slot />
</article>
</main>
.markdown-container {
& >*:not(:first-child) {
margin-block-start: var(--flow-space);
}
& > :is(.card, .card-grid):not(:first-child) {
margin-block-start: var(--space-l);
}
& >div>p:not(:first-child) {
margin-block-start: var(--flow-space);
}
& > :is(h1, h2, h3):not(:first-child) {
margin-block-start: 1.75em;
}
:where(p:not(.eyebrow)) {
color: var(--color-text);
}
a {
color: var(--color-link);
&:hover {
color: var(--color-link-hover);
}
}
a:is([href^="http://"], [href^="https://"]):not(.button)::after {
content: "";
display: inline-block;
inline-size: 0.8em;
block-size: 0.8em;
margin-inline-start: 0.25em;
background-color: currentColor;
mask: url("/icons/external-link.svg") no-repeat center / contain;
}
img {
block-size: auto;
margin-inline: auto;
border-radius: var(--border-radius-s);
}
details {
color: var(--color-text);
border: var(--border-width-s) solid var(--color-border);
border-radius: var(--border-radius-m);
}
details[open] {
padding-block-end: var(--space-m);
padding-inline: var(--space-m);
}
summary {
padding-block: var(--space-s);
padding-inline: var(--space-m);
border-radius: var(--border-radius-m);
cursor: pointer;
transition: var(--transition-normal) background-color;
&::marker {
color: var(--color-link);
}
&:hover {
background-color: var(--color-surface);
}
}
details[open] > summary {
margin-inline: calc(var(--space-m) * -1);
border-block-end: var(--border-width-s) solid var(--color-border);
border-end-start-radius: 0;
border-end-end-radius: 0;
}
details > summary + * {
margin-block-start: var(--space-m);
}
details > :not(summary) + :not(summary) {
margin-block-start: var(--flow-space);
}
details > :not(summary) + :is(h1, h2, h3) {
margin-block-start: 1.75em;
}
blockquote {
padding-block: var(--space-s);
padding-inline: var(--space-m);
background-color: var(--color-surface);
border-inline-start: var(--border-width-s) solid var(--color-accent-500);
border-start-start-radius: 0;
border-start-end-radius: var(--border-radius-m);
border-end-start-radius: 0;
border-end-end-radius: var(--border-radius-m);
& > * + * {
margin-block-start: var(--flow-space);
}
footer {
margin-block-start: var(--space-xs);
}
}
ol, ul {
padding-inline-start: 1em;
}
li {
overflow-wrap: anywhere;
margin-bottom: 0.5em;
}
table {
display: block;
inline-size: fit-content;
max-inline-size: 100%;
margin-inline: auto;
overflow: auto;
border: var(--border-width-s) solid var(--color-border);
border-collapse: collapse;
border-spacing: 0;
td,
th {
border: var(--border-width-s) solid var(--color-border);
}
td {
padding-block: var(--space-2xs);
padding-inline: var(--space-xs);
}
th {
padding-block: var(--space-s);
padding-inline: var(--space-m);
font-weight: var(--font-weight-semi-bold);
text-align: start;
}
caption {
padding-block-start: var(--space-xs);
color: var(--color-text-muted);
font-size: var(--font-size-s);
text-align: start;
caption-side: bottom;
}
tbody tr:nth-child(odd) {
background-color: var(--color-surface);
code:not(pre code) {
background-color: var(--color-background);
}
}
}
hr {
border: 0;
border-block-end: var(--border-width-s) solid var(--color-border);
}
code:not(pre code) {
padding-inline: 1px;
color: var(--color-link);
background-color: var(--color-surface);
border-radius: var(--border-radius-s);
}
pre {
max-inline-size: 100%;
padding-block: var(--space-xs);
padding-inline: var(--space-s);
overflow-x: auto;
background-color: var(--color-surface);
border-radius: var(--border-radius-s);
font-size: var(--font-size-code);
tab-size: 2;
code {
font-size: inherit;
}
}
pre.astro-code {
background-color: var(--color-surface) !important;
}
kbd {
padding-inline: var(--space-2xs);
color: var(--color-text);
background-color: var(--color-surface);
border: var(--border-width-s) solid var(--color-border);
border-block-end-width: var(--border-width-m);
border-radius: var(--border-radius-s);
font-family: var(--font-mono);
font-size: var(--font-size-s);
white-space: nowrap;
}
mark {
padding-inline: 0.1em;
color: var(--color-text);
background-color: var(--color-surface-muted);
background-color: color-mix(in srgb, var(--color-accent-500) 35%, transparent);
}
abbr[title] {
text-decoration-style: dotted;
text-underline-offset: 0.15em;
cursor: help;
}
cite {
color: var(--color-text-subtle);
}
sup > [data-footnote-ref] {
font-size: inherit;
}
& > .footnotes {
margin-block-start: var(--space-l);
padding-block-start: var(--space-m);
color: var(--color-text-subtle);
border-block-start: var(--border-width-s) solid var(--color-border);
:where(p, li, a) {
font-size: var(--font-size-s);
}
[data-footnote-backref] {
margin-inline-start: var(--space-2xs);
text-decoration: none;
}
& > .sr-only {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
}
}

The first rule gives each direct child some space from the element before it. Headings and card components need larger gaps, so they get specific rules afterwards.

The div > p selector handles paragraphs that might be wrapped in another element.

:where(p:not(.eyebrow)) has zero selector specificity. A component class such as .eyebrow can keep its own text color without fighting the article rule.

External links get an icon through a mask. The mask uses currentColor, so it follows the link’s hover color. Add the icon at public/icons/external-link.svg, or remove this rule. Buttons are excluded.

Images keep their natural aspect ratio, cannot grow past the width set by the reset and are centred when they are narrower than the article.

details needs separate open and closed styles. When it opens, the summary uses negative inline margins to reach the outer border while the remaining content keeps its padding.

Blockquotes use a thicker visual edge on the inline start side. Logical properties make this work in left-to-right and right-to-left documents.

Lists get enough inline padding for their markers. Long list item content can wrap anywhere.

Tables become scrollable when they are wider than their container. display: block is necessary for overflow: auto to work. The table itself can stay at its content width when it is small.

Inline code and code blocks are selected separately. code:not(pre code) styles code inside a sentence. The nested code rule inside pre only makes it inherit the block’s size.

When using Astro: Astro’s syntax highlighter writes an inline background on .astro-code, so that one rule needs !important if the site surface color should win.

The file also covers keyboard input, highlighted text, abbreviations, citations and generated footnotes.

Highlighted text uses --color-surface-muted as a fallback. Browsers that support color-mix() use the current accent color instead.


Sources and references