The syntax: one character away from a link

In short
  • The syntax is ![alt text](path), and the leading exclamation mark is the only thing separating an image from a link.
  • Markdown stores a reference, never the file, so a broken image is nearly always a broken path.
  • Plain Markdown has no width setting at all - An HTML <img> tag is the only portable way to size a picture.
  • One relative path can work on GitHub, break in a site build, and still look perfect in your local preview.

An image in Markdown is ![alt text](path/to/image.png). It is the same shape as a link, with an exclamation mark in front. That single line is the entire syntax:

![A red bicycle leaning against a brick wall](images/bike.jpg)

The three parts

The leading ! means "embed this, do not link to it". The square brackets hold the alt text. The parentheses hold the path or URL. Drop the exclamation mark and you get a text link instead of a picture. That is the most common typo here, and the reason it pays to know link syntax first.

Tip: To show the syntax on screen without rendering it, wrap it in a code block. Backticks switch every marker off, including the exclamation mark.

Adding a title

![Quarterly revenue chart](charts/q3.png "Revenue, Q3 2025")

A quoted string after the path becomes the HTML title attribute. Desktop browsers show it as a tooltip. Most writers skip it, and with reason: touch devices never show it, screen readers treat it in different ways, and search engines ignore it. Use one for a photo credit, never for something the reader needs.

What it becomes

<img src="images/bike.jpg" alt="A red bicycle leaning against a brick wall">

That single line explains everything below. Markdown never copies, uploads or embeds the file. It stores a reference. So a broken image is almost always a broken path rather than broken syntax. See Markdown vs HTML for how the two languages fit together.

Illustration of a Markdown link gaining an exclamation mark and turning into a displayed photograph

Alt text: what it is actually for

Alt text is not a caption and not a filename. It has two jobs. Screen readers announce it in place of the picture, so for a blind reader the alt text is the image. It is also what the browser prints when the file 404s, when a firewall blocks the CDN, or when someone reads your README in a terminal.

Write what the image shows

![Export menu with the "Download .html" button highlighted](docs/export.png)

Compare that with ![screenshot](docs/export.png). The first tells a reader who cannot see the picture what they are missing. The second tells them nothing at all. Three habits carry most of the weight:

  • Describe the content, not the file. "Bar chart of sign-ups by month" beats "chart.png" every time.
  • Skip the words "image of". Assistive software already announces that it is an image.
  • Keep it to one phrase. A short sentence is plenty. A paragraph is a caption, not alt text.

Charts and diagrams get one extra rule. If the picture carries data your prose does not, put the finding itself in the alt text. An image is the one element in a Markdown document that cannot be read as plain text.

Tip: When the numbers matter more than the picture, write a Markdown table instead of pasting a screenshot of one. A table stays readable, searchable and copyable in every reader.

When empty alt text is the right answer

![](images/section-divider.svg)

Purely decorative images should have empty brackets. That is correct, not lazy. Empty alt tells a screen reader to skip something meaningless. Writing ![divider] makes every listener hear the word "divider" for no reason. If removing the image would cost the reader nothing, leave the brackets empty.

Relative paths vs absolute URLs

An absolute URL starts with https:// and works from anywhere. The price is a dependency on a host you may not control. A relative path is resolved against wherever the Markdown file currently sits. Images then travel with a Git repository and survive every branch and fork - Until you paste the same Markdown somewhere the folder does not exist.

![Diagram](img/build.png)          same folder, then img/
![Diagram](./img/build.png)        identical, the ./ is decorative
![Diagram](../assets/build.png)    up one folder, then assets/
![Diagram](/img/build.png)         from the root, and this is the risky one
![Diagram](https://cdn.example.com/build.png)   works anywhere online

The same path in three places

Relative paths confuse people because "relative to what?" has a different answer in every tool:

Path you writeGitHub repo viewStatic site build (Hugo, Jekyll, Astro)Local preview (VS Code)
img/build.pngResolves next to the .md file. Works.Usually breaks: the built page sits at a different URL depth than the source.Works if the folder really is beside the file.
../assets/build.pngWorks, and is common in docs subfolders.Breaks. Build output has no ../ matching your source tree.Works.
/img/build.pngResolved against the repo root. Fine on github.com, dead in a raw clone.The correct form: a leading slash means the published site root.Resolved against the workspace folder, which flatters you.
https://cdn.example.com/x.pngWorks, proxied through GitHub's image cache.Works.Works when you are online.
img/My Photo.pngBroken. The space ends the path.Broken.Broken.

Two rules that cover most projects

In a repository README, use paths relative to the file. In a static site, put images under static or public and reference them from the site root with a leading slash. The complete guide shows both patterns in a finished document.

Watch out: Never mix the two conventions in one project, and never treat a working local preview as proof. VS Code resolves a leading slash against your workspace folder, so it looks right up to the moment you deploy.

Illustration comparing a document pointing to an image in a nearby folder against one pointing to a remote server

Sizing images, which Markdown cannot do

Markdown's image syntax has no width and no height. Not a shorthand, not an attribute, nothing. An image renders at its natural pixel size, which is why an untouched phone screenshot swallows an entire README. To set a size you have to step outside Markdown. Three workarounds exist, in descending order of portability.

1. Resize the file before you commit it

The boring answer is the best one. A 900-pixel-wide PNG loads faster, prints correctly and works in every renderer. It needs no special syntax, so nothing can strip it and nothing can ignore it.

2. Use a raw HTML img tag

<img src="docs/settings.png" alt="Settings page" width="420">

<p align="center">
  <img src="docs/logo.svg" alt="Project logo" width="180">
</p>

This is the answer on GitHub, and the closest thing to a portable one. Nearly every renderer that allows HTML accepts it: GitHub, GitLab, VS Code and most static site generators. Three details decide whether it behaves:

  • Set only width and the height scales with it. A percentage such as width="50%" works too.
  • Keep the tag on its own line, with a blank line above and below it.
  • Markdown inside an HTML block is not processed, so do not nest other syntax in there.

Watch out: Strict renderers and publishing systems that sanitise HTML strip the tag outright. The image then vanishes rather than shrinking, which is one more argument for resizing the file. Markdown vs HTML covers which tools allow what.

3. Query strings, and what GitHub really supports

A query string is passed to whatever server hosts the image, so it only does something if that server understands it. An image CDN such as Cloudinary or imgix will resize on request:

![Hero](https://cdn.example.com/hero.jpg?w=600&fm=webp)

GitHub understands one useful parameter, ?raw=true. It turns a repository page URL into the file itself:

![Chart](https://github.com/acme/docs/blob/main/img/chart.png?raw=true)

That is the fix when an image shows as a broken icon because you copied the address bar from a file page. A blob URL returns an HTML page, not an image. The direct equivalent is raw.githubusercontent.com/acme/docs/main/img/chart.png. GitHub has no width parameter, so ?w=400 is simply ignored. It does support one fragment trick: #gh-dark-mode-only and #gh-light-mode-only give per-theme artwork.

Editor-specific shorthands

Several tools invented their own sizing syntax, and none of it is standard:

  • Obsidian takes ![alt|300](img.png).
  • Typora and markdown-it-imsize take ![alt](img.png =250x).
  • Pandoc takes ![alt](img.png){width=50%}.

None of it renders outside its own tool, and the reader sees the raw characters instead. That is fine for private notes and wrong for anything you publish. The Obsidian guide is the place to weigh up its house extensions.

Where to actually put the file

Choosing a host is choosing who is responsible when the image disappears. Link rot is not hypothetical. Hosts change terms, free tiers close, and a URL that worked in 2019 is a broken icon today.

OptionBest forLink-rot riskHonest trade-off
Folder in the repo (docs/img/)READMEs, project docsVery lowVersioned, reviewable, works offline. Bloats repo size, and binary diffs are useless.
GitHub drag-and-drop into an issue or commentBug reports, quick screenshotsMediumZero effort, instantly hosted. The file lives outside your repo, so a clone or a move to another host shows nothing.
Static site static/ or public/Blogs, docs sitesVery lowDeploys with the site, URLs are yours. Needs root-relative paths, which do not preview in a repo view.
Object storage or image CDN (S3, R2, Cloudinary)Large or numerous images, resizing on the flyLow, if the bill is paidFast, resizable, effectively unlimited. Costs money and adds a service to keep alive.
Free host such as imgurForum posts, throwaway sharingHighFree and instant. Terms change, old uploads get purged, hotlinking may be blocked, and offices firewall it.
Hotlinking someone else's pageNothingVery highSpends their bandwidth without consent, and they can rename, delete or replace the file with anything at all.

For anything in a Git repository, commit the images. It is the only option where the picture and the prose are certain to stay together. That is the same reasoning that makes plain text worth using at all, set out in full in what Markdown is.

Tip: Keep every asset for a project in one folder, such as docs/img/. Scattered images are why moving a single file breaks half a README, and one folder turns the repair into a single find-and-replace.

SVG, GIF and formats that behave differently

The syntax does not care about file format. It only writes an src. Whether the picture displays depends entirely on the viewer.

  • PNG and JPEG work everywhere. PNG for screenshots and diagrams, JPEG for photographs.
  • Animated GIF works everywhere and animates by default, which makes it the standard way to demo a short UI interaction. Keep clips to a few seconds; a 20 MB GIF is a slow page for everyone.
  • SVG renders on GitHub, GitLab and in browsers and stays crisp at any size, so it suits logos and diagrams. Scripts inside an SVG are stripped for security, and some renderers refuse SVG outright.
  • WebP and AVIF are far smaller than PNG and work in current browsers and GitHub, but older tooling still chokes on them.
  • PDFs and video are not images. ![](demo.mp4) renders a broken image, not a player. Point at them with ordinary link syntax, or use GitHub's upload box, which turns a dragged-in video into a player.

When in doubt, choose PNG for anything containing text and JPEG for photographs. Both have worked in every tool for decades, and neither needs a fallback. Support varies by platform, and the cheat sheet lists which ones render images at all.

Common mistakes and how to fix them

Spaces in the filename

Broken:     ![Team photo](img/team photo 2025.jpg)
Fixed:      ![Team photo](img/team-photo-2025.jpg)
Also works: ![Team photo](img/team%20photo%202025.jpg)
Also works: ![Team photo](<img/team photo 2025.jpg>)

The parser stops at the first space. Percent-encoding or angle brackets rescue a file that already exists. Renaming to hyphens stops the problem coming back.

Case sensitivity

Broken on a server:  ![Logo](img/Logo.PNG)
File on disk:        img/logo.png

macOS and Windows filesystems ignore case by default. The Linux servers that build and serve your site do not. So a page that previews flawlessly on a laptop can deploy with every image broken. Adopt one convention - Lowercase and hyphens for every asset - And the whole class of bug disappears.

Full-width brackets from a Chinese IME

Broken:  ![圖表](img/chart.png)
Fixed:   ![圖表](img/chart.png)

A Chinese input method produces full-width punctuation, and Markdown recognises only half-width ASCII. The alt text can be in any language. The four punctuation marks around it cannot. Switch to half-width before typing syntax, or type the punctuation first and paste the text in.

The missing exclamation mark, and blob URLs

Broken:  [Screenshot](img/app.png)          renders as a text link
Fixed:   ![Screenshot](img/app.png)

Broken:  ![Chart](https://github.com/a/b/blob/main/c.png)
Fixed:   ![Chart](https://github.com/a/b/blob/main/c.png?raw=true)

Both are one-character repairs. The first is a missing marker. The second is a URL that points at a web page rather than at the file behind it.

A fault table for the rest

What you seeUsual causeFix
A text link where a picture should beThe ! is missingAdd the exclamation mark
Broken icon on GitHub, fine on your laptopFilename case, or a blob page URLMatch the case exactly, or add ?raw=true
Broken icon on the live site, fine in the repoPath is relative to the file, not the site rootMove the file under static/ and start the path with /
Alt text shows, the image never loadsNothing is at that pathOpen the URL on its own in a browser tab
The whole line prints as plain textFull-width brackets from an IMERetype the four punctuation marks in half-width

Work down that table before blaming the platform. A wrong URL 404s the moment you open it, and a working one means the fault is in your Markdown. For a shorter walkthrough see how to add an image in Markdown.

Support data verified

Frequently Asked Questions

How do I resize an image in Markdown?

You cannot, not with Markdown syntax. Use a raw HTML tag such as <img src="a.png" alt="..." width="420">, which GitHub and most renderers accept. Resizing the file before you add it is safer still. Shorthands like ![alt|300](a.png) work only in the tool that invented them.

Why is my image showing as a broken icon on GitHub?

Three usual suspects. The path is relative to the wrong folder, the filename case does not match, or you pasted a blob page URL instead of a raw one. Linux servers care about case and your laptop does not. Add ?raw=true to a blob URL, then open the address on its own to check the file is really there.

Should I ever leave the alt text empty?

Yes, for purely decorative images. Empty brackets tell a screen reader to skip the image, which is exactly right for a divider or a flourish. For anything that carries meaning, describe what the reader would otherwise miss.

Can I put an image inside a table cell or a list?

Yes to both. Image syntax is inline, so it works inside a table cell and inside a list item. Watch the width in tables, since a full-size screenshot will stretch the column past the edge of the page.

Do images work the same on every platform?

No. GitHub, GitLab, Obsidian and VS Code support the full syntax. Discord and Reddit do not render it, and expect an upload or a bare URL instead. The cheat sheet has the platform-by-platform picture.

Does Markdown support video embeds?

No. Markdown has no syntax for video at all, and ![demo](demo.mp4) gives you a broken image rather than a player. GitHub is the exception worth knowing: drag a video file into an issue, a pull request or a README and it renders a player, which is a GitHub feature and not part of Markdown. Everywhere else you need a raw HTML <video> tag, commonly stripped by the sanitizers platforms run for security, or a thumbnail image wrapped in a link that points at the video.

Keep learning

Try the Editor

Try the Editor