<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Justin Levine</title>
    <description>The latest articles on DEV Community by Justin Levine (@jal-co).</description>
    <link>https://dev.to/jal-co</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3626758%2F0ae18167-49c5-46ad-8de0-956cccb06e8d.png</url>
      <title>DEV Community: Justin Levine</title>
      <link>https://dev.to/jal-co</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jal-co"/>
    <language>en</language>
    <item>
      <title>I built a shields.io alternative that renders badges as shadcn/ui buttons</title>
      <dc:creator>Justin Levine</dc:creator>
      <pubDate>Sat, 25 Apr 2026 22:02:06 +0000</pubDate>
      <link>https://dev.to/jal-co/i-built-a-shieldsio-alternative-that-renders-badges-as-shadcnui-buttons-4bhn</link>
      <guid>https://dev.to/jal-co/i-built-a-shieldsio-alternative-that-renders-badges-as-shadcnui-buttons-4bhn</guid>
      <description>&lt;p&gt;README badges have looked the same for a decade. Flat rectangles, basic colors, that shields.io aesthetic. They work, but if you're building a project with shadcn/ui or any modern component library, the badges are always the part that looks out of place.&lt;/p&gt;

&lt;p&gt;I wanted badges that looked like they belonged in the same design system as everything else. So I built &lt;a href="https://shieldcn.dev/?ref=dev.to"&gt;shieldcn&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Every badge is a real shadcn/ui Button component rendered to SVG via &lt;a href="https://github.com/vercel/satori" rel="noopener noreferrer"&gt;Satori&lt;/a&gt;. Same Inter font, same border-radius, same padding, same color tokens per variant. You get a URL, you put it in your README, it looks like a button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;npm&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://shieldcn.dev/npm/react.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://shieldcn.dev/github/stars/vercel/next.js.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;discord&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://shieldcn.dev/discord/1316199667142496307.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All the shadcn Button variants work: default, secondary, outline, ghost, destructive. There's also a &lt;code&gt;branded&lt;/code&gt; variant that pulls the icon's brand color automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;branded&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://shieldcn.dev/npm/react.svg?variant=branded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://shieldcn.dev/npm/react.svg?variant=outline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;ghost&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://shieldcn.dev/npm/react.svg?variant=ghost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The interesting constraint is that SVGs embedded as &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags are completely sandboxed. No external stylesheets, no CSS variables, no JavaScript. So you can't use &lt;code&gt;var(--primary)&lt;/code&gt; or any of the usual shadcn theming. Every color has to be resolved to a literal hex value before rendering.&lt;/p&gt;

&lt;p&gt;I extracted every shadcn Button token into a lookup table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;darkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModeColors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fafafa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;primaryForeground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#18181b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#27272a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secondaryForeground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fafafa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;destructive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#dc2626&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a single &lt;code&gt;resolve()&lt;/code&gt; function takes the variant, size, mode, theme, and any color overrides, computes every value, and passes it all to the renderer. The renderer itself has zero branching per variant. It just receives hex values and lays out the badge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// resolve() computes ALL colors before rendering&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// One render path for every variant&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderSingle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps things consistent. Adding a new variant means adding a row to the token table, not touching the render logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Satori quirks
&lt;/h2&gt;

&lt;p&gt;A few things I ran into using Satori for this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No &lt;code&gt;opacity&lt;/code&gt; CSS property.&lt;/strong&gt; Satori silently ignores it. I use &lt;code&gt;rgba()&lt;/code&gt; with baked-in alpha instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`rgba(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;.&lt;/strong&gt; Every SVG icon has to be parsed into a React element tree before Satori can render it. I wrote a lightweight SVG parser that converts raw SVG strings into nested &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;circle&amp;gt;&lt;/code&gt;, etc. elements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Font loading matters.&lt;/strong&gt; In a Next.js Route Handler you need to load fonts from the filesystem with &lt;code&gt;readFileSync&lt;/code&gt;, not fetch them from a URL. I pre-load all font files at module scope so they're cached across requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The whole app is one Next.js catch-all route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It parses the URL into a provider + params, fetches data, resolves colors, renders the badge, and returns SVG (or PNG via @resvg/resvg-wasm, or JSON).&lt;/p&gt;

&lt;p&gt;Provider functions live in &lt;code&gt;lib/providers/&lt;/code&gt; and each one returns the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The renderer doesn't know or care where the data came from. It just gets a label, a value, and some colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's covered
&lt;/h2&gt;

&lt;p&gt;25+ data providers right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Package registries: npm, PyPI, Crates.io, Docker Hub, Packagist, RubyGems, NuGet, Pub.dev, Homebrew, Maven, CocoaPods, JSR, Bundlephobia&lt;/li&gt;
&lt;li&gt;Code platforms: GitHub (stars, CI, issues, PRs, releases, downloads, license, and a bunch more), Codecov, VS Code Marketplace&lt;/li&gt;
&lt;li&gt;Social: Discord, Reddit, Bluesky, YouTube, Mastodon, Lemmy, Hacker News&lt;/li&gt;
&lt;li&gt;Custom: static badges, dynamic JSON (point at any API), HTTPS endpoint proxy, memo badges (PUT your own data)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;40,000+ icons from SimpleIcons, Lucide, and React Icons. You can also upload a custom SVG via base64 data URI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token pool
&lt;/h2&gt;

&lt;p&gt;GitHub's API rate limit is 60 requests/hour for unauthenticated requests. That's nothing for a badge service. shields.io solved this with a token pool where users donate OAuth tokens, and I borrowed the same approach.&lt;/p&gt;

&lt;p&gt;Users authorize a GitHub OAuth app (read-only, zero scopes, revocable anytime) and their token gets added to a pool stored in Postgres. API requests get distributed across all the tokens in the pool. More tokens = more capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  shadcn registry
&lt;/h2&gt;

&lt;p&gt;There's also a &lt;a href="https://shieldcn.dev/docs/registry" rel="noopener noreferrer"&gt;component registry&lt;/a&gt; if you want to use badge components in your own app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dlx shadcn@latest add &lt;span class="s2"&gt;"https://shieldcn.dev/r/readme-badge.json"&lt;/span&gt;
pnpm dlx shadcn@latest add &lt;span class="s2"&gt;"https://shieldcn.dev/r/readme-badge-row.json"&lt;/span&gt;
pnpm dlx shadcn@latest add &lt;span class="s2"&gt;"https://shieldcn.dev/r/badge-preview.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Homepage + badge builder: &lt;a href="https://shieldcn.dev" rel="noopener noreferrer"&gt;shieldcn.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://shieldcn.dev/docs" rel="noopener noreferrer"&gt;shieldcn.dev/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/jal-co/shieldcn" rel="noopener noreferrer"&gt;github.com/jal-co/shieldcn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed, everything is free, PRs welcome. Would love to see you guys use it. &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>resources</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
