Having good alt text is an obvious good when creating and maintaining websites. However, it is an often-overlooked task, partly because it’s quite a tedious task. Nevertheless, it should be done with care and intention.
This blog post will showcase a new workflow that I have found that makes creating and maintaining good alt text a much more manageable task. Creating alt text occupies this weird space of tasks that feel like they should be automatable, but have just enough complexity that it becomes too hard to do. That is where agents like Claude code provide a lot of their value, being able to handle the tedious parts while leaving the human to do what we do best.
Quarto
Generating figures in a quarto file or projects gives us a lot of things to take advantage of.
All of this work assumes that you have chunk labels, preferably with cross reference prefix fig-. The workflow will likely still work, but having this done certainly helps, as we can use grep to easily identify chunks.
The first and likely most important thing we can use is the code that generates the figures themselves. Since alt text is fairly formulaic, it can be helpful to be able to extract basic information from the code instead of the resulting image. Things like scatterplot, histogram, and linechart can easily be inferred from the code, as can the x, y, color, and facet groups.
We can also use the rendered image itself, which can easily be found using the chunk label and file names.
Additionally, we can parse the prose surrounding the code chunk itself. This can help avoid some duplication of information between the prose and alt text, but also help to inform the alt text by peeking into previous chunks.
All in all, by using Quarto in a principled way, we can programmatically tie an image to the code that was used to create it. as well as the text the reader sees before and after the image. This has, for me, been enough to reliably create decent alt text.
The skill
Claude Code supports custom slash commands (also called “skills”), which are markdown files that give Claude specific instructions for a task. To use this skill, create a file at .claude/commands/write-chart-alt-text.md in your project directory.
Below is the skill I have been using. It follows Amy Cesal’s three-part formula for writing alt text: chart type, data description, and key insight.
write-chart-alt-text.md
# Write Chart Alt Text
Generate accessible alt text for data visualizations in this project.
ARGUMENTS
- label: (optional) specific fig- label to generate alt text for
- file: (optional) specific .qmd file to process
## Instructions
When invoked, analyze the figure(s) and generate alt text following these guidelines:
### Key Advantage: Source Code Access
Unlike typical alt text scenarios where you only see an image, **we have access to the R code that generates each chart**. Use this to extract precise details:
**From `ggplot2` code:**
- `aes(x, y)` → exact variable names for axes
- `aes(color = ...)` / `aes(fill = ...)` → what color encodes
- `geom_point()` → scatter, `geom_histogram()` → histogram, `geom_line()` → line chart
- `geom_smooth()` / `geom_abline()` → overlaid fitted lines
- `facet_wrap(~var)` → number of panels and what varies
- `scale_color_gradient()` → color encoding scheme
- `labs(x = ..., y = ...)` → axis labels if customized
**From data generation code:**
- `rbeta()`, `rnorm()`, `runif()` → expected distribution shape
- `mutate()` transformations → what was done to data
- Recipe steps → feature engineering applied
- Filtering/subsetting → what subset is shown
**From surrounding prose:**
- Text before/after the chunk explains the **purpose** and **key insight**
- Chapter context tells you what the figure is meant to teach
- This is often the best source for the "key insight" part of alt text
### Three-Part Structure (Amy Cesal's Formula)
1. **Chart type** - First words identify the format
2. **Data description** - Axes, variables, what's shown
3. **Key insight** - The pattern or takeaway (often found in surrounding text)
### Relationship to fig-cap
Read the `fig-cap` first. The alt text should **complement, not duplicate** it:
- If caption states the insight, alt text can focus on describing the visual structure
- If caption is generic, alt text should include the key insight
- Together they should give a complete understanding
### Content Rules
**Include:**
- Chart type as first words
- Axis labels and what they represent
- Specific values/ranges when code reveals them (e.g., "peaks between 25-50")
- Number of panels/facets
- What color/size encodes if used
- The key pattern that supports the chapter's point
**Exclude:**
- "Image of..." or "Chart showing..." (screen readers announce this)
- Decorative color descriptions (unless color encodes data)
- Information already in fig-cap
- Implementation details (package names, function internals)
### Length Guidelines
| Complexity | Sentences | When to use |
|------------|-----------|---------------------------------------------|
| Simple | 2-3 | Single geom, no facets, obvious pattern |
| Standard | 3-4 | Multiple geoms or color encoding |
| Complex | 4-5 | Faceted, multiple overlays, nuanced insight |
### Quality Checklist
- [ ] Starts with chart type (Scatter chart, Histogram, Faceted bar chart, etc.)
- [ ] Names the axis variables
- [ ] Includes specific values/ranges from code when informative
- [ ] States the key insight from surrounding prose
- [ ] Complements (not duplicates) the fig-cap
- [ ] Would make sense to someone who cannot see the image
- [ ] Uses plain language (avoid jargon like "geom" or "aesthetic")
## Template Patterns
**Scatter chart:**
```
Scatter chart. [X var] along the x-axis, [Y var] along the y-axis.
[Shape: linear/curved/clustered]. [Specific pattern, e.g., "peaks when X is 25-50"].
[Any overlaid fits or annotations].
```
**Histogram:**
```
Histogram of [variable]. [Shape: right-skewed/bimodal/normal/uniform].
[If transformed: "after [transformation], the distribution [result]"].
[Notable features: outliers, gaps, multiple modes].
```
**Bar chart:**
```
Bar chart. [Categories] along the x-axis, [measure] along the y-axis.
[Key comparison: which is highest/lowest, relative differences].
[Pattern: increasing/decreasing/grouped].
```
**Tile/raster chart:**
```
Tile chart [or heatmap]. [Row variable] along the y-axis, [column variable] along the x-axis.
Color encodes [what value]. [Pattern: where values are high/low].
[If faceted: "N panels showing [what varies]"].
```
**Faceted chart:**
```
Faceted [chart type] with [N] panels, one per [faceting variable].
[What's constant across panels]. [What changes/varies].
[Key comparison or insight across panels].
```
**Correlation heatmap:**
```
Correlation [matrix/heatmap] of [what variables]. [Arrangement].
[Overall pattern: mostly positive/negative/mixed].
[Notable clusters or strong/weak pairs].
[If relevant: contrast with expected behavior, e.g., "unlike PCA, these are not orthogonal"].
```
**Before/after comparison:**
```
[N] [chart type]s arranged [vertically/in grid]. [Top/Left] shows [original].
[Bottom/Right] shows [transformed]. [Key difference/similarity].
[If overlay: "[color] curve shows [reference]"].
```
**Line chart with overlays:**
```
[Line/Scatter] chart with overlaid [fits/curves]. [Axes].
[Number] of [lines/fits] shown: [list what each represents].
[Which fits well vs. poorly and why].
```
## Workflow
### Finding Figures
To find all figure chunks in the project:
```bash
# List all figure labels with file and line number
grep -n "#| label: fig-" *.qmd
# Find figures in a specific file
grep -n "#| label: fig-" numeric-splines.qmd
# Find a specific figure
grep -rn "#| label: fig-splines-predictor-outcome" *.qmd
```
### For Each Figure
1. **Locate** - Use grep to find file and line number
2. **Read context** - Read ~50 lines around the chunk (prose before + code + prose after)
3. **Extract details** - Note fig-cap, ggplot code, data generation, surrounding explanation
4. **Draft alt text** - Apply three-part structure (type → data → insight)
5. **Verify** - Check against quality checklist
## Example
**Code context:**
```r
plotting_data |>
ggplot(aes(value)) +
geom_histogram(binwidth = 0.2) +
facet_grid(name~., scales = "free_y") +
geom_line(aes(x, y), data = norm_curve, color = "green4")
```
**Surrounding prose says:** "Normalization doesn't make data more normal"
**fig-cap:** "Normalization doesn't make data more normal. The green curve indicates the density of the unit normal distribution."
**Good alt text:**
```
#| fig-alt: |
#| Faceted histogram with two panels stacked vertically. Top panel shows
#| original data with a bimodal distribution. Bottom panel shows the same
#| data after z-score normalization, retaining the bimodal shape. A green
#| normal distribution curve overlaid on the bottom panel clearly does not
#| match the data, demonstrating that normalization preserves distribution
#| shape rather than creating normality.
```In use
Once I want to add or make sure my alt text is looking good, I spin up Claude Code and go “Yo use the write-chart-alt-text.md skill”. And it will go and work through all your figures, providing updated suggestions where it sees fit.
Don’t blindly trust AI output; it can make mistakes just as easily as you can. Always review and adjust the alt text as you see fit.
And you are done! Call quarto render and boom, new functional alt text.
What I found really helpful is that my Claude right now doesn’t nitpick when using this skill. After going through and modifying the alt texts that I felt needed it, I spun up another Cluade in a different session and had it perform the skill on the newly generated alt texts. And it didn’t make any changes as they are all “good enough”. I’m very happy with that as I don’t want to look at 100s of minor changes each time I do this.
The Shiny App
This last section isn’t needed to use the skill, just something I ended up doing as well. I felt it was worth sharing. I knew I wanted to verify that all alt text was visible. So I vibe coded up a shiny app to look at all the new alt texts side-by-side with the figures themselves.
I also had it put a link in for me that would take me to the exact chunk that I was looking at. This allowed me to look at all the figures and their alt text, while letting me quickly jump directly to the code if I wanted to modify one of them.