SVG Is Just Markup: Building a Dynamic Badge Graphic Entirely in Drupal's Theme Layer
March 11, 2026About 3:00 p.m. on Friday before a holiday, a content editor walks into your office. They need the promotional badge updated — different title, new color, smaller font because the campaign name is longer this time. You've been here before. Someone opens Illustrator, exports a PNG, uploads it, clears cache, and forty-five minutes have evaporated for what amounts to changing two words and a hex code.
Now multiply that by every locale, every campaign, every "can we just try it in blue?"
There's a better way. And it doesn't require a single line of custom PHP, no JavaScript rendering library, and zero image files.
The architectural insight is so stupid simple that it's almost embarrassing: SVG is just XML markup. Twig already knows how to handle markup. That means Drupal's field system can become a real-time graphic design tool — if you wire it up right.
TL;DR
This approach lets content editors modify dynamic graphics (colors, text, icons, sizing) directly through Drupal's admin UI. No designer bottleneck, no image file management, no third-party service costs. It's built entirely with Drupal's native field system and theme layer.
The Dynamic Badge Block Architecture
The foundation is a Custom Content Block type with the machine name dynamic_badge_block. The config lives at block_content.type.dynamic_badge_block.yml — exportable, deployable, version-controlled. No custom module required. This approach uses the existing field system in addition to the theme layer.
In my example, seven fields do all the work:
| Field | Machine Name | Purpose |
|---|---|---|
| Badge Title | field_pendant_title | The text rendered dynamically inside the SVG |
| Font Size | field_pendant_font_size | Manual control over title text size within the SVG |
| Theme Color | field_theme_color | Sets the badge's color theme — injected as a fill or CSS custom property |
| Icon | field_badge_icon | Selects which icon from the embedded SVG sprite to display |
| Drop Shadow | field_has_shadow | Boolean toggle for shadow rendering |
| Body Text | field_badge_body | Description text associated with the badge |
| Wrapper Size | field_wrapper_size | Controls overall badge dimensions |
Each field has its own config definition — field.field.block_content.dynamic_badge_block.field_pendant_title.yml, and so on — defining defaults, allowed values for list fields, and required states. The form display config (core.entity_form_display.block_content.dynamic_badge_block.default.yml) controls what editors see.
Here's where it gets interesting. The field_pendant_font_size field exists for a reason that trips people up: SVG text doesn't reflow like HTML text. There's no `word-wrap`. No responsive line breaks. If an editor types a longer campaign title, the text will cheerfully overflow the badge boundary and clip into oblivion. Giving editors explicit font-size control is the pragmatic fix. It's not elegant, but it works — and it's better than the alternative of filing a Jira ticket every time the copy changes.
Performance Win: Because this is inline SVG, you aren't just saving time—you're saving round-trips to the server. For your mobile users on spotty connections, that badge appears instantly with the rest of the DOM. No "pop-in," no layout shift.
Templates Are Made To Be Wired
This is the core of the pattern. The Twig template lives at: web/themes/custom/{your_theme_name}/templates/block/block--block-content--type--dynamic-badge-block.html.twig
That naming convention — block--block-content--type--{bundle}.html.twig — follows Drupal's suggestion pattern and targets every block content instance of the dynamic_badge_block type.
The SVG markup lives entirely in this Twig template. This is the critical concept. It's not an <img> tag referencing a file. It's not an <object> embed. It's inline SVG — raw XML that Twig renders as part of the page markup. That's what makes dynamic injection possible.
Think of it like this: if you can put a field value into an <h2>, you can put it into an SVG <text> element. If you can set a CSS class dynamically, you can set a fill attribute.
The icon field, however, deserves a specific callout. Rather than storing image files, field_badge_icon holds a symbol ID that maps to an embedded SVG sprite — rendered once in the page via html.html.twig or a dedicated include. The browser resolves <use href="#{{ icon_id }}"> locally. Zero additional HTTP requests, fully self-contained. The field_has_shadow boolean works on the same principle — a simple true/false value that conditionally injects both the <defs> filter definition and the filter attribute on the shape using CSS. No JavaScript toggle needed.
The Gotcha That Will Break Your SVG
Here's the part I wish someone had told me the first time.
By default, Drupal's field formatters wrap output in HTML tags. A rendered field_pendant_title doesn't give you "Spring Campaign" — it gives you something like:
<div class="field field--name-field-pendant-title">
<div class="field__item"><span>Spring Campaign</span></div>
</div>Drop that inside an SVG <text> element and your badge renders... nothing. No error in the console. No big red warning. The SVG just silently fails because <div> is not valid SVG content. You'll stare at an empty circle wondering what you did wrong. Ask me how I know.
Two escape routes:
Option 1: Bypass rendered output entirely. Access the raw entity value in Twig:
{% set badge_title = block_content.field_pendant_title.value %}This gives you the raw, unadulterated string. No wrappers. No opinions. Just the data you asked for.
Option 2: Configure the view display. In core.entity_view_display.block_content.dynamic_badge_block.default.yml, set the field formatter to "Plain text" and labels to "Hidden". This reduces the wrapper markup, though you may still get a <div class="field__item"> depending on your theme's field templates.
For SVG attribute injection — fill, font-size, viewBox dimensions — Option 1 is the only reliable path. You need a raw value, not a render array.
Why This Pattern Matters Beyond Badges
Zoom out for a second. My badge is a nice demo, but the architectural pattern is the real prize.
Any SVG can be driven by Drupal fields. Progress indicators where the percentage comes from a computed field. Organizational charts built from entity references. Branded assets where marketing controls the color palette without a deployment. Data visualization where the numbers update when content does.
This is the kind of problem teams usually throw a JavaScript charting library or a third-party image generation API at. Both add dependencies, complexity, and often cost. The Drupal way to handle this is to recognize that you already have the plumbing: a structured content model, a rendering pipeline, and a template engine that doesn't care whether it's outputting <div> or <svg>. Simple and direct.
And a bonus that Drupal provides in core. There's a language.content_settings.block_content.dynamic_badge_block.yml config file in this setup — the block type is translation-aware. That means your dynamic badge title, body text, and any localizable field values can be translated through Drupal's standard translation workflow. Your SVG graphics are localizable out of the box. Try getting that from a folder full of exported PNGs.
Stop treating SVGs like static images and start treating them like the flexible markup they are!
What’s the most 'hacky' way you’ve solved a dynamic graphic problem in the past? Let’s swap horror stories in the comments.
Don't let your theme layer go to waste.
SVG is just the beginning. Let's talk about how we can unlock your Drupal site’s full potential without adding technical debt.
0 Comments
Login or Register to post comments.