Skip to main content
JoshuaBriley.
Back to blog
Design Systems 6 min read

Measuring style guide adoption

A specification for measuring Rudiment UI adoption across an organization using the rudi namespace.

A specification for measuring Rudiment UI adoption with the rudi namespace: why it’s the measurement foundation, what rudi check emits, and how to roll it out through GitHub Actions.

Why the namespace is the measurement instrument

In React codebases, a component’s identity lives in import statements, which get aliased, wrapped, re-exported, and renamed. A scanner has to trace module graphs and guess whether <Button> is a design system button, wrapper, or local recreation. Mews found import-based measurement inaccurate for this reason.

Custom elements dissolve that problem. The platform requires every custom element name to contain a hyphen, and registration is global, so <rudi-button> means the same thing in every file, template language, and framework: no alias, no rename, no ambiguity.

  • Identity is stable at the usage site. A scanner matches the tag name. It doesn’t trace imports, resolve barrels, or guess about wrappers.
  • The namespace survives every layer. The same rudi- string appears in source templates, JSX, Vue and Svelte templates, Lit template literals, server-rendered HTML, and the live DOM. You can measure at build time with static analysis or at runtime with querySelectorAll, and the numbers reconcile.
  • Recreation detection becomes a set difference. Every hyphenated tag is a component by definition. Partition all custom element tags into rudi-* and everything else. The “everything else” set is your recreated-component report, with names attached.
  • Tokens inherit the same property. --rudi-color-* custom properties are collision-free and greppable. Any var(--rudi-*) reference is adoption. Any hardcoded color value in product CSS is measurable drift.
  • The namespace works with zero tooling. GitHub code search for rudi- across the organization gives you a rough adoption census before you build anything. The CLI below makes that structured, repeatable, and trackable.
  • Framework-agnostic measurement matches a framework-agnostic library. Rudiment’s rewrite to web components means consumers may be in React, Vue, Astro, or plain HTML. A tag-based scanner covers all of them with one strategy. React-centric tools like react-scanner and Radius Tracker can’t offer that.

One limitation: static scanning misses tags composed at runtime from dynamic strings. This is rare, and an optional later phase, runtime DOM sampling, closes the gap.

The rudi check CLI

rudi check is a read-only scanner. Run it at the root of any repository. It parses source files, counts namespace usage and drift signals, and emits one JSON report.

What it scans

SourceWhat it extracts
.html, .astro, .vue, .svelte, .jsx, .tsx, .js, .tsHyphenated element tags, split into rudi-* and foreign; bare interactive HTML elements (button, dialog, input, select, details); attributes passed per rudi-* element
Tagged template literals (Lit html, css)Same tag and attribute extraction inside the literal
.css, and <style> blocksvar(--rudi-*) references; non-token color values (hex, rgb(), hsl(), oklch()); --rudi-* overrides
package.json and lockfileDeclared and resolved versions of @rudi/* packages

Counting rule: count source-level references, not rendered instances. A rudi-button inside a shared header counts once, no matter how many pages render it. Repeat that in every report, or the numbers get misread.

Output schema

{
  "schemaVersion": 1,
  "generatedAt": "2026-07-21T06:00:00Z",
  "repo": "acme/checkout",
  "commit": "9f2c1ab",
  "scanner": { "name": "@rudi/check", "version": "0.1.0" },

  "versions": {
    "declared": { "@rudi/core": "^2.3.0" },
    "resolved": { "@rudi/core": "2.3.1" }
  },

  "elements": {
    "rudi-button": {
      "count": 42,
      "files": 17,
      "attributes": { "variant": 40, "disabled": 12, "class": 31 }
    },
    "rudi-dialog": { "count": 6, "files": 4, "attributes": { "open": 6 } }
  },

  "tokens": {
    "references": { "--rudi-color-surface": 12, "--rudi-space-3": 44 },
    "overrides": { "--rudi-color-accent": 2 }
  },

  "foreign": {
    "customElements": { "acme-card": 9, "acme-toggle": 3 },
    "bareInteractive": { "button": 4, "dialog": 1 },
    "hardcodedColors": { "count": 23, "files": 9 }
  },

  "coverage": {
    "elementCoverage": 0.87,
    "tokenCoverage": 0.79
  }
}

Field notes:

  • schemaVersion lets you evolve the format without breaking the dashboard. Bump it on any breaking change.
  • elements[*].attributes is the web components equivalent of prop tracking: a high class or style count signals that an element’s defaults don’t fit, and a never-used attribute is a removal candidate for the next major version.
  • foreign.customElements is the recreation report. Names are included on purpose: “the checkout team built acme-toggle, used 3 times” starts a specific conversation, and doubles as roadmap demand data.
  • elementCoverage = rudi element references ÷ (rudi + foreign custom elements + bare interactive elements). tokenCoverage = token references ÷ (token references + hardcoded color values).
  • Version data rides along in every report, so version-lag tracking needs no separate pipeline.

Commands

rudi check                  # human-readable summary to stdout
rudi check --report out.json  # machine-readable report
rudi check --ignore "**/legacy/**"

Ship it as a report-only tool first. Don’t fail builds on adoption thresholds. Measurement tooling that blocks merges gets uninstalled; enforcement, if you want it, belongs in a later phase after teams trust the numbers.

GitHub Actions rollout

Use a central scanner rather than asking every team to install a workflow. Distributed per-repo workflows are a later phase, once teams want the report in their own PRs.

  1. Publish @rudi/check to your registry.
  2. Create a rudiment-metrics repository to hold reports and the dashboard.
  3. Add a scheduled workflow (weekly is enough) that lists organization repositories through the GitHub API, shallow-clones each, runs rudi check --report, and commits the output to data/<repo>/<date>.json.
  4. Build the dashboard as a static page that reads the JSON history. Astro fits, and it can live in the Field Guide, next to the docs that improve it.
  5. Later, offer an opt-in reusable workflow so teams can surface their own report as a PR comment.

Minimal workflow sketch:

name: rudiment-adoption-scan
on:
  schedule:
    - cron: '0 6 * * 1'
  workflow_dispatch:
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @rudi/check
      - run: node scripts/scan-org.js # clones repos, runs rudi check, writes data/
        env:
          GH_TOKEN: ${{ secrets.ORG_READ_TOKEN }}
      - run: |
          git config user.name "rudi-bot"
          git config user.email "rudi-bot@users.noreply.github.com"
          git add data && git commit -m "chore: weekly adoption scan" && git push

Questions the data answers

  • Version lag: resolved @rudi/core versions across repositories, and how long each lags behind.
  • Component health: usage per component, attribute histograms, and override rates, ranked for improve-or-deprecate calls.
  • Recreation rate: foreign custom elements per repository, trending down (ideally) as gaps get filled.
  • Token drift: hardcoded color counts per repository, an early sign the theme system doesn’t meet a need.
  • Coverage trend: element and token coverage per repository, week over week.

Right-sizing

This spec scales down as well as up. At current Rudiment scale, phase one is a single script plus GitHub code search. The JSON schema is worth getting right early, since history only accumulates from the day you start recording it. The enterprise patterns (central scanning, dashboards, deprecation codemods) layer on without rework, since the namespace contract never changes.