Skip to main content
info@drupalodyssey.com
Wednesday, March 11, 2026
Contact

Main navigation

  • Home
  • Services
  • Case Studies
  • Blog
  • Resources
  • About
Search
Development

SVG Is Just Markup: Building a Dynamic Badge Graphic Entirely in Drupal's Theme Layer

March 11, 2026

About 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.

SVG badge example

See the post Shortlink Manager Evolved: Smarter Analytics, QR Codes, and AI-Driven Development for a live implementation.

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.

  • Twig template contains all of the SVG code.
  • Embedded SVG sprite for the icons.
  • Dynamic SVG badge.
  • Block configuration for the SVG fields.

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.

Build Smarter Today

SVG is just the beginning. Let's talk about how we can unlock your Drupal site’s full potential without adding technical debt.

Author

Ron Ferguson

 

Next Blog

0 Comments

Login or Register to post comments.

Ad - Header (728*90 AD)

Ad - Sidebar (300 x 600 AD)

Ad - Sidebar (300 x 250 AD)

Newsletter

Subscribe my Newsletter for new blog and tips.

Menu

  • Home
  • Services
  • Case Studies
  • Blog
  • Resources
  • About

Legal

  • Privacy Policy
  • Terms & Conditions
  • Disclaimer
  • Cookies

I specialize in custom development, performance tuning, and reliable maintenance, delivering clean code and strategic solutions that scale with your business. Ready to discuss your project?

E: info@drupalodyssey.com
Fort Worth, TX

© 2026 All Rights Reserved.

Proud supporter of active military, veterans and first responders.