You open the media library, find an image with no alt text, type a proper description, click Update, and reload the page it sits on. The HTML has not changed. Nothing is broken and no cache is at fault. WordPress is doing exactly what it was built to do.
Alt text in WordPress lives in one place and gets copied to another. It lives in a row of post meta on the attachment. It gets copied into the block markup in post_content at the moment you insert the image, and that copy never looks back at the original.
Once you know that, the subject stops being mysterious. An alt text audit is two audits, a fix is two fixes, and the size of the problem is one SQL query away. On this site, 684 image attachments produced 274 with no usable alt text, which is 40 percent.
The advice everyone repeats, describe the image, is the easy half. Here is the other half first.
Where WordPress keeps alt text
Alt text is a single row in the postmeta table with the meta key _wp_attachment_image_alt, attached to the attachment post ID. That is the whole storage mechanism. There is no column for it, no separate table, and no dedicated API beyond ordinary get_post_meta() and update_post_meta().
It is not stored in the image file. JPEG and PNG carry their own metadata blocks, but WordPress never writes alt text into them and never reads it out, which is a separate subject: what EXIF and IPTC data survive an upload. It is also not any of the three fields the media modal stacks next to it, which is where most of the confusion starts.
Every write path in core lands on that same key. Uploads and the REST API, which is what the block editor’s media modal talks to, run the value through sanitize_text_field(). The classic attachment edit screen runs it through wp_strip_all_tags() in wp-admin/includes/post.php. Either way, alt text is one line of plain text and any markup you paste is stripped before it is stored. Reading it back is a single query.
SELECT meta_value
FROM mv0DiJnB_postmeta
WHERE post_id = 1343
AND meta_key = '_wp_attachment_image_alt';
That mv0DiJnB_ is this site’s table prefix, set by $table_prefix in wp-config.php. Substitute yours in every query below.
The four fields people confuse
The media modal stacks four text fields in a column with almost no indication that they do completely different jobs. Three of them are columns on the attachment post. Only one becomes the alt attribute.
- Title is
post_title, filled from the file name on upload, which is why so many libraries are full of titles like “img 4471”. It is what the media library search matches, so keep it sane, but it does not appear on the page and it describes nothing. - Caption is
post_excerpt, the one field here that is visibly published. It renders in the<figcaption>under the image, for everybody. Write it for sighted readers. - Description is
post_content. It appears on the attachment page, which most sites never link to and many themes render badly. In practice it is a private note. - Alternative Text is the
_wp_attachment_image_altmeta. The only one that becomes thealtattribute, and the only one read out in place of the image.
The common failure is writing a good description into Description, watching it save, and assuming the accessibility box is ticked. It is stored, it is retrievable, and no visitor will ever meet it.
The copy that explains everything
Knowing which box to type in is only half of it. The rest happens at the moment you insert the image.
When you open the media modal, wp_prepare_attachment_for_js() builds a JavaScript object for each attachment carrying 'alt' => get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ) alongside the title, caption and description. The editor now holds the current value.
When you click Insert, the block editor writes an image block into post_content, and the alt goes in as a literal HTML attribute. A real block from this site looks like this, trimmed for width.
<!-- wp:image {"id":1343,"sizeSlug":"full","linkDestination":"none"} -->
<figure class="wp-block-image size-full">
<img src="https://example.com/wp-content/uploads/2026/07/logo.png"
alt="The WunderPaint logo in dark blue"
class="wp-image-1343"/>
</figure>
<!-- /wp:image -->
The attachment ID is right there twice, as "id":1343 in the block attributes and as the wp-image-1343 class. WordPress knows exactly which attachment this is. It still never goes back for the alt text.
Core confirms it twice. The render callback in wp-includes/blocks/image.php calls $processor->get_attribute( 'alt' ) to build its lightbox label, pulling the alt back out of the saved markup rather than out of the meta. And wp_filter_content_tags(), which rewrites content images on the way to the browser, adds srcset, sizes, loading, decoding and fetchpriority, and touches alt not at all.
So the rule, stated plainly: the alt text you type in the media library is copied into the post at insert time, and editing the library value afterwards changes nothing about images already placed in a post. Almost every report of “I fixed my alt text and nothing happened” is this, and nothing else.
Which images update and which do not
The copy is not universal. The question is whether the HTML was written into the database once or is generated fresh on every request, and anything going through wp_get_attachment_image() is generated fresh. Its default attributes, in wp-includes/media.php:
$default_attr = array(
'src' => $src,
'class' => "attachment-$size_class size-$size_class",
'alt' => trim( strip_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ),
);
A fresh get_post_meta() on every page load. That splits every image on your site into three groups.
- Live from the meta: featured images, Post Featured Image blocks, image widgets, anything a theme or plugin renders with
wp_get_attachment_image(). These update the moment you save the library value. - Frozen at insert time: images placed in the block editor, images placed in the classic editor, and anything else stored as raw HTML in
post_content. - Stored somewhere else entirely: page builders, which keep their own JSON in post meta and copy the alt in exactly the same way, just into a different field.
One considerate detail: when WordPress creates a cropped derivative, wp-admin/includes/image.php copies both the caption and _wp_attachment_image_alt from the parent attachment, so the derived image inherits the description. What else one upload turns into is a longer story.
The consequence is what to take away. An alt text audit has two halves: the attachment meta, and the markup already sitting in post_content. A library reporting 100 percent coverage can still serve pages full of empty alt attributes, because those pages were written before it was fixed.
Measuring the library half
The count that matters is not “images without alt text” but “images without usable alt text”, and it needs a LEFT JOIN, because an attachment that has never had alt text has no meta row at all and an inner join silently hides it.
SELECT
COUNT(*) AS images,
SUM(m.meta_id IS NULL) AS no_alt_row,
SUM(m.meta_id IS NOT NULL AND TRIM(m.meta_value) = '') AS empty_alt,
ROUND(100 * SUM(m.meta_id IS NULL OR TRIM(m.meta_value) = '') / COUNT(*), 1) AS pct_unusable
FROM mv0DiJnB_posts p
LEFT JOIN mv0DiJnB_postmeta m
ON m.post_id = p.ID
AND m.meta_key = '_wp_attachment_image_alt'
WHERE p.post_type = 'attachment'
AND p.post_status = 'inherit'
AND p.post_mime_type LIKE 'image/%';
Here that returns 677 live images, 266 with no meta row, 1 with an empty string, 39.4 percent unusable. Drop the post_status line and the totals become 684 and 274, or 40.1 percent, because seven image attachments sit in the trash. Worth knowing before you quote a figure: inherit is the normal status for an attachment, and trashed attachments keep their rows.
The same numbers through WP-CLI, if SQL makes you nervous. These are ordinary read-only WP_Query calls.
# total images in the library
wp post list --post_type=attachment --post_mime_type=image --format=count
# images with no alt meta row at all
wp post list --post_type=attachment --post_mime_type=image
--meta_key=_wp_attachment_image_alt --meta_compare='NOT EXISTS'
--format=count
# the same set as a worklist, with IDs and file URLs
wp post list --post_type=attachment --post_mime_type=image
--meta_key=_wp_attachment_image_alt --meta_compare='NOT EXISTS'
--fields=ID,post_title,guid --format=csv
WP-CLI returned 677 and 266, matching the SQL, because wp post list excludes the trash by default. If your two methods disagree by a small number, the trash is the first place to look.
The last query is the one people skip and usually the most revealing. It finds alt text that exists but is only the file name in disguise, which is what a badly configured bulk tool leaves behind.
SELECT p.ID, m.meta_value AS alt, p.guid
FROM mv0DiJnB_posts p
JOIN mv0DiJnB_postmeta m
ON m.post_id = p.ID
AND m.meta_key = '_wp_attachment_image_alt'
WHERE p.post_type = 'attachment'
AND p.post_mime_type LIKE 'image/%'
AND TRIM(m.meta_value) != ''
AND (
m.meta_value REGEXP '\.(jpe?g|png|gif|webp|avif)$'
OR m.meta_value REGEXP '^(IMG|DSC|DCIM|PXL|Screenshot)[ _-]?[0-9]'
OR LOWER(REPLACE(TRIM(m.meta_value), ' ', '-'))
= LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(p.guid, '/', -1), '.', 1))
)
LIMIT 100;
Three tests in one: alt text carrying a file extension, alt text starting like a camera or phone file name, and alt text that is the slug of its own file with the hyphens turned into spaces. This site returned zero rows, which is the answer you want. A site that has been through an automatic “fill in all the alt text” plugin often returns hundreds, each one a field that looks complete to a reporting tool and reads as gibberish to a person.
Measuring the markup half
Now the images already placed in posts. This is coarser, because you are pattern matching HTML in a text column rather than querying structured data, but it sizes the problem.
SELECT ID, post_title, post_type
FROM mv0DiJnB_posts
WHERE post_status = 'publish'
AND post_type IN ('post', 'page')
AND post_content LIKE '%<img %'
AND (post_content NOT LIKE '% alt=%'
OR post_content LIKE '%alt=""%')
LIMIT 100;
Two caveats, because a query that lies is worse than no query. It flags a post if any image in it lacks alt, not which one, so it is a triage list rather than a fix list. And alt="" is correct for a decorative image, so the second condition finds candidates, not defects. Here the first condition returned zero posts and the second returned six, all deliberate.
For a real report you want the rendered page, because themes and page builders inject images that never appear in post_content. Run an accessibility checker over the live URLs and use the SQL as a cross-check: when the checker flags a page whose stored content looks clean, the fix belongs in the theme.
Measuring the markup half by hand stops being realistic somewhere around the twentieth image. Paste the HTML of a post below and the auditor lists every image with what its alt attribute actually contains.
It flags the ones that matter: no alt at all, alt that is just the file name, alt that starts with “image of”, alt duplicated across a dozen images, and the one that catches most people, an empty alt on an image that is itself a link, which leaves the link with no accessible name at all.
Alt text auditor
Paste the HTML of a post and get one row per image, with the alt text faults that actually turn up in a WordPress library: no alt at all, the file name repeated, the caption copied, or a sentence nobody would want read out. The HTML is parsed in a detached document, no picture is ever requested, and nothing leaves this browser tab.
or press Enter to pick one. The file is read in this tab and never uploaded.
| File | Alt text | Length | Findings |
|---|
Rows with faults come first. Grey marks are notes, not faults.
- Say what the picture shows for the paragraph it sits next to, in one sentence, and stop there.
- Leave the alt empty when the picture only decorates: an empty alt is a decision, a missing one is an oversight.
- Stay under 125 characters and drop "image of", the file name and the list of keywords.
- When the picture is the only thing inside a link, the alt has to name where the link goes.
Filling them in without making it worse
The honest warning first. Wrong alt text is worse than no alt text. A missing attribute is a gap a listener recognises as a gap and a checker can flag. Confident, plausible, incorrect alt text is a lie delivered in a calm voice, and nobody downstream can detect it. The temptation with 266 empty fields is one UPDATE copying the file name into all of them. That produces text which is technically present and practically worthless, and it destroys the signal telling you which images still need a human.
The file name is not a description either. hook-head-lighthouse.jpg is a good file name and poor alt text, because it says what is in the frame and nothing about why the picture is here. An order that works:
- Take a database backup. Everything below writes to
postmetaorpost_content, both easy to get wrong at scale. - Filter to images actually used somewhere. An orphan on no page needs no alt text, and cutting the list down is the difference between a finite job and an infinite one. Clearing out a media library covers how to establish which attachments are genuinely in use.
- Sort what remains by the traffic of the pages they appear on.
- Work down that list starting with functional images and images of text, which cost the most when missing.
- Mark decorative images with an explicit empty alt rather than leaving the field blank by accident, so the job can be finished.
- Then go back through the posts containing those images and replace the markup, because steps 2 to 5 only fixed the library.
Step 6 is the one everybody forgets. In the block editor, select the image block and retype the alt in the block sidebar, which writes straight into the markup. Across many posts a careful search and replace over post_content works, but match on the wp-image-NNN class so you target one specific attachment rather than a URL that appears in several sizes. Run that as a one-off WP-CLI command rather than something you leave installed: where custom code belongs is worth deciding deliberately.
For the library half at scale, WunderPaint’s media library manager collects everything with missing alt text into a smart folder and can generate titles, alt text and descriptions in bulk, which turns the impossible version of this job into an afternoon of editing rather than removing the editing step.
Whatever writes the first draft, remember what it cannot know. A tool can see a person at a desk. It cannot know the picture is there to show the desk.
Who is reading it
Three audiences, in order of how much they matter.
People using a screen reader. Someone navigating a page by ear, where every image either contributes a sentence to the story or interrupts it. This is the audience the field exists for.
Anyone whose image did not load. Slow connection, blocked host, a file that went missing in a migration. The alt text stands in for the picture.
Search engines. Real, but third. Alt text is one of several signals used to work out what an image shows, alongside the file name, the caption and the surrounding text. Writing for this audience first is how you end up with keyword-stuffed nonsense that helps nobody.
The question that makes it easy
Before writing anything, ask: if this image vanished, what would the reader lose?
Sometimes the answer is nothing, because the image is decorative. Sometimes it is a specific fact, because the image is a chart. Sometimes it is a mood, because the image sets a scene. Each answer produces a different kind of alt text, and getting the classification right is most of the work.
Decorative images get empty alt text
A background texture, a divider, a stock photograph of hands on a keyboard next to an article about productivity. These carry no information the text does not already carry.
The correct alt text is empty: alt="". Not missing, empty, and the difference is not pedantry. An empty attribute tells a screen reader that the author considered this image and decided it carries nothing, so skip it silently. A missing attribute says nothing, so the reader falls back on announcing the file name, and nobody needs to hear “d s c underscore zero four four seven one dot j p g”.
WordPress stores “deliberately empty” and “never filled in” identically, so core cannot tell decorative from not-done-yet. That is why the query above counts both as unusable, and why the decision has to be recorded by a person in the markup rather than inferred from the library.
Informative images get the information
The picture carries something the text does not. Describe that something, not the picture.
Compare “photo of a shoe” against “the sole of the running shoe, showing the two-density foam layers”. Both are true. Only one tells you why the picture was included. A product photograph should describe what a buyer is trying to see, usually the colour, the material and the angle.
Functional images get the function
If the image is a link or a button, the alt text is not a description, it is the destination. An icon of a house linking to the front page should be “home”, not “house icon”. A magnifier that opens search should be “search”. Describing the drawing rather than the action is the most common accessibility mistake in navigation, and no automated tool will catch it, because the attribute is present and the words are accurate.
Charts and diagrams get the finding
You cannot fit a chart into alt text, and you should not try. Give the conclusion in the alt attribute, then put the underlying data somewhere reachable: a caption, a table below, a link.
“Bar chart showing monthly signups” is nearly useless. “Signups roughly doubled between March and June, then held steady” is what a sighted reader takes away in one glance, so it is what the alt text should say.
Images of text get the text
If a picture contains words, the words go in the alt text, verbatim. A quotation card, a poster, a screenshot of an error message. This one has no nuance, is frequently skipped, and is the case where a missing alt removes information that exists nowhere else on the page.
Practical rules
- Do not start with “image of” or “photo of”. Screen readers already announce that it is an image. You are wasting the listener’s first two words.
- Keep it to about one sentence. There is no hard limit in the specification, but long alt text is read as an uninterrupted block with no way to skim. If you need more, you want a caption or body text.
- Punctuate it. A full stop makes a screen reader pause. Without one, the alt runs into whatever follows.
- Do not repeat the caption. Both get read out, and hearing the same sentence twice is worse than hearing it once. When the caption already says it, the alt can often be empty.
- Context decides. The same lighthouse photograph might be empty on a homepage, “the lighthouse at Hook Head, seen from the cliff path” in a travel article, and “the black and white band pattern used to identify the tower by day” in a piece about navigation. There is no correct alt text for an image in isolation.
That last rule redeems the mechanism this article opened with. One attachment holds one alt text, but the same image can appear on five pages doing five different jobs. The insert-time copy, annoying as it is for bulk fixes, is what makes per-page alt text possible: the value in the markup can differ from the library value, deliberately. Treat the library value as the sensible default and the block value as the one that is published.
Alt text and SEO, honestly
Alt text is not a keyword field. It is an accessibility field that search engines happen to read, and that distinction decides every judgement call you will make about it.
Write the honest description and the keywords take care of themselves, because a genuine description of a picture on a page about a subject naturally contains the words of that subject. Stuffing has not worked for years. “Running shoes, best running shoes, cheap running shoes, buy running shoes online” is detected easily and is a downgrade rather than a neutral experiment. If your alt text would sound absurd read aloud, it is wrong, and being read aloud is literally its job.
Two things do help and cost nothing. Name the file properly before uploading, because hook-head-lighthouse-cliff-path.jpg beats IMG_4471.jpg in image search and in your own media library six months later. And put the image near text about the same subject, because the surrounding paragraph has always been a stronger signal than the alt attribute.
Symptom and cause
I fixed the alt text in the media library and the page still shows the old text. The image was inserted into the post before you made the edit, so the old value is baked into post_content. Edit the alt in the block sidebar on that post, not in the library.
The featured image picked up my change but the image in the article did not. Two code paths. The featured image goes through wp_get_attachment_image(), which reads the meta on every request. The in-content image is stored HTML.
A screen reader reads out a long file name where an image should be. The alt attribute is missing entirely, not empty. If the image is decorative, add alt="". Assistive technology treats the two completely differently.
A checker reports missing alt on a page where every image in the library has alt text. The images come from a template or a page builder rendering its own markup, so they never touched the library value. Find the source of the markup before editing anything in the media library.
Every image has alt text and it all reads like a file path. A bulk tool populated the field from the file name. Run the file-name query above. Alt text that is present and semantically empty passes every automated audit and fails every human one.
The read-aloud test
One test is worth more than every checker. Read the page aloud, substituting your alt text wherever an image appears. If it flows as a piece of writing, the alt text is right. If it stutters, repeats the caption, or announces things that do not matter, you now know which images to fix. No tool can do this, because the question is not what the picture shows but what the sentence around it needed.
The technical half is smaller than it looks. One meta key, _wp_attachment_image_alt, holds every alt text on the site. One copy at insert time explains every case where an edit appears not to have worked. A handful of read-only queries measure both halves, which here came to 274 unusable out of 684 images, a number that would otherwise have stayed a vague worry.
The writing half does not scale, and it should not. Alt text is the only place in a WordPress install where you have to state in one sentence why a picture is on the page. Plenty of images survive that question badly, and finding out which ones is worth more than filling 274 fields with text nobody wants to hear.