diff --git a/docs/syntax/images.md b/docs/syntax/images.md
index 63f765436..684d8b97a 100644
--- a/docs/syntax/images.md
+++ b/docs/syntax/images.md
@@ -77,6 +77,30 @@ Screenshots are images displayed with a box-shadow. Define a screenshot by addin
:screenshot:
:::
+## Borders
+
+Images can have a border to improve contrast with the page background. Add the `:border:` attribute to a block-level image directive to add a 1px border.
+
+```markdown
+:::{image} /syntax/images/observability.png
+:alt: Elasticsearch with border
+:width: 400px
+:border:
+:::
+```
+
+:::{image} /syntax/images/observability.png
+:alt: Elasticsearch with border
+:width: 400px
+:border:
+:::
+
+The border is especially useful for screenshots with white or light backgrounds that might blend with the documentation page.
+
+:::{note}
+The `:border:` option only applies to images used with the image directive. Inline images do not support borders.
+:::
+
## Inline images
```markdown
diff --git a/src/Elastic.Documentation.Site/Assets/markdown/images.css b/src/Elastic.Documentation.Site/Assets/markdown/images.css
index e5c4e62d4..1cf33670d 100644
--- a/src/Elastic.Documentation.Site/Assets/markdown/images.css
+++ b/src/Elastic.Documentation.Site/Assets/markdown/images.css
@@ -21,5 +21,8 @@
.screenshot {
@apply border-grey-20 bg-grey-10 border-1 p-4 shadow-md;
}
+ .border {
+ @apply border-grey-20 border-1;
+ }
}
}
diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
index 7b2721c80..43c762f3e 100644
--- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
+++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
@@ -126,6 +126,7 @@ private static void WriteImage(HtmlRenderer renderer, ImageBlock block)
Target = block.Target,
Width = block.Width,
Screenshot = block.Screenshot,
+ Border = block.Border,
ImageUrl = imageUrl,
});
RenderRazorSlice(slice, renderer);
@@ -147,6 +148,7 @@ private static void WriteImageCarousel(HtmlRenderer renderer, ImageCarouselBlock
Width = img.Width,
Scale = img.Scale ?? string.Empty,
Screenshot = img.Screenshot,
+ Border = img.Border,
Target = img.Target,
ImageUrl = img.ImageUrl
}).ToList(),
@@ -191,6 +193,7 @@ private static void WriteFigure(HtmlRenderer renderer, ImageBlock block)
Target = block.Target,
Width = block.Width,
Screenshot = block.Screenshot,
+ Border = block.Border,
ImageUrl = imageUrl,
});
RenderRazorSlice(slice, renderer);
diff --git a/src/Elastic.Markdown/Myst/Directives/Image/ImageBlock.cs b/src/Elastic.Markdown/Myst/Directives/Image/ImageBlock.cs
index e9ede80fe..9939db988 100644
--- a/src/Elastic.Markdown/Myst/Directives/Image/ImageBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Image/ImageBlock.cs
@@ -45,6 +45,11 @@ public class ImageBlock(DirectiveBlockParser parser, ParserContext context)
///
public string? Screenshot { get; set; }
+ ///
+ /// When set, adds a border to the image.
+ ///
+ public string? Border { get; set; }
+
///
/// The uniform scaling factor of the image. The default is “100 %”, i.e. no scaling.
///
@@ -88,6 +93,9 @@ public override void FinalizeAndValidate(ParserContext context)
// Set Screenshot to "screenshot" if the :screenshot: option is present
Screenshot = Prop("screenshot") != null ? "screenshot" : null;
+ // Set Border to "border" if the :border: option is present
+ Border = Prop("border") != null ? "border" : null;
+
ExtractImageUrl(context);
}
diff --git a/src/Elastic.Markdown/Myst/Directives/Image/ImageView.cshtml b/src/Elastic.Markdown/Myst/Directives/Image/ImageView.cshtml
index 04ab147ba..650bd73c5 100644
--- a/src/Elastic.Markdown/Myst/Directives/Image/ImageView.cshtml
+++ b/src/Elastic.Markdown/Myst/Directives/Image/ImageView.cshtml
@@ -1,6 +1,9 @@
@inherits RazorSlice
+@{
+ var cssClasses = string.Join(" ", new[] { Model.Screenshot, Model.Border }.Where(c => !string.IsNullOrEmpty(c)));
+}
-
+
@Model.RenderBlock()
diff --git a/src/Elastic.Markdown/Myst/Directives/Image/ImageViewModel.cs b/src/Elastic.Markdown/Myst/Directives/Image/ImageViewModel.cs
index 95ed7ee2e..478af0927 100644
--- a/src/Elastic.Markdown/Myst/Directives/Image/ImageViewModel.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Image/ImageViewModel.cs
@@ -26,6 +26,7 @@ public class ImageViewModel : DirectiveViewModel
? Guid.NewGuid().ToString("N")[..8] // fallback to a random ID if ImageUrl is null or empty
: ShortId.Create(ImageUrl);
public required string? Screenshot { get; init; }
+ public required string? Border { get; init; }
public string Style
{
diff --git a/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs b/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs
index efeeb6a63..981e5ba56 100644
--- a/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs
@@ -66,3 +66,36 @@ public void WarnsOnExternalUri()
.And.OnlyContain(d => d.Severity == Severity.Warning);
}
}
+
+public class ImageBlockWithBorderTests(ITestOutputHelper output) : DirectiveTest(output,
+"""
+:::{image} img/observability.png
+:alt: Elasticsearch
+:width: 400px
+:border:
+:::
+"""
+)
+{
+ protected override void AddToFileSystem(MockFileSystem fileSystem) =>
+ fileSystem.AddFile(@"docs/img/observability.png", "");
+
+ [Fact]
+ public void ParsesBlock() => Block.Should().NotBeNull();
+
+ [Fact]
+ public void ParsesBorderProperty()
+ {
+ Block!.Alt.Should().Be("Elasticsearch");
+ Block!.Width.Should().Be("400px");
+ Block!.ImageUrl.Should().Be("/img/observability.png");
+ Block!.Border.Should().Be("border");
+ }
+
+ [Fact]
+ public void ImageIsFoundSoNoErrorIsEmitted()
+ {
+ Block!.Found.Should().BeTrue();
+ Collector.Diagnostics.Count.Should().Be(0);
+ }
+}