Third Party Widget

A small impact summary for a third-party stack — aggregates scripts by category, paints a donut (or stacked bar) of the breakdown, and lists each script with its share of the total. Use it to teach where third-party time goes by kind (analytics vs ads vs chat) rather than to audit specific rows.

For a sortable ledger with per-script bars and a budget verdict, reach for Third Party Audit — this primitive deliberately stays smaller.

Preview

Third-party impact

Main-thread cost grouped by category.

Total impact580ms
  • analytics180ms (31%)
  • ads270ms (47%)
  • chat95ms (16%)
  • social35ms (6%)
  • analytics.jsanalytics110ms (19%)
  • segment.jsanalytics70ms (12%)
  • ads.jsads180ms (31%)
  • doubleclick.jsads90ms (16%)
  • intercom.jschat95ms (16%)
  • twitter-embed.jssocial35ms (6%)
Customize
Viz
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/third-party-widget.json

Usage

import { ThirdPartyWidget } from "@craft-bits/core";
 
<ThirdPartyWidget
  scripts={[
    { id: "ga", label: "analytics.js", impactMs: 110, category: "analytics" },
    { id: "ads", label: "ads.js", impactMs: 180, category: "ads" },
    { id: "intercom", label: "intercom.js", impactMs: 95, category: "chat" },
  ]}
/>

Stacked-bar variant with explicit category ordering:

<ThirdPartyWidget
  title="Where the JS time goes"
  scripts={scripts}
  viz="bar"
  categories={[
    { id: "ads", label: "Ads" },
    { id: "analytics", label: "Analytics" },
    { id: "chat", label: "Chat" },
    { id: "social", label: "Social" },
  ]}
/>

Anatomy

  • Header. Optional title (rendered with the cb-label style) and a description sub-line. Omit both for a chromeless panel.
  • Viz. A donut split into category slices (default) or a horizontal stacked bar. Each slice / segment uses one tone from the accent palette, cycled by category index.
  • Legend. Category chips with absolute and percentage share. Hide with hideLegend.
  • Per-script list. A compact one-line-per-script list under the viz with the category tone dot and a percentage share. Hide with hideScriptList.

Understanding the component

  1. Aggregation. On every render the component groups scripts by category into a stable ordered list. Category order comes from the categories prop first (insertion order), then falls back to first-seen order in scripts. Negative impactMs values clamp to zero so the donut math stays sane.
  2. Tone cycling. Each category gets one of five token tones — accent, success, warning, error, muted — by index. Pass categories[].tone to pin a category to a specific slot. The same tone is reused on the slice, the legend dot, and the per-script row.
  3. Donut. The slices are rendered as a single SVG with stacked circle strokes — each slice gets a strokeDasharray sized to its share and a strokeDashoffset that advances the cursor around the ring. The total reads through the centre.
  4. Bar. The stacked bar lays each category as a flex segment whose width is its share of the total. The bar grows from zero on mount with SPRINGS.smooth; reduced-motion users see it instantly.
  5. Per-script rows. Each row enters once with SPRINGS.snap and a tiny y shift. The category dot and the percentage share recolour to match the category's tone slot.

Props

PropTypeDefaultDescription
scriptsThirdPartyWidgetScript[]requiredScripts to summarise.
titleReactNodeOptional heading above the panel.
descriptionReactNodeOptional sub-headline under the title.
categoriesThirdPartyWidgetCategory[]Override category order, labels, and tone slots.
viz'donut' | 'bar''donut'Visualisation style.
unitstring'ms'Unit suffix on the totals readout.
totalLabelReactNode'Total impact'Label rendered next to the total.
hideScriptListbooleanfalseHide the per-script list.
hideLegendbooleanfalseHide the legend chips.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

ThirdPartyWidgetScript

FieldTypeDescription
idstringStable identifier, used as React key.
labelReactNodeHuman-readable script label.
impactMsnumberMain-thread impact in milliseconds.
categorystringCategory bucket — matches a categories[].id.

ThirdPartyWidgetCategory

FieldTypeDescription
idstringMatches script.category.
labelReactNodeDisplay label for the legend. Defaults to id.
tonenumberTone slot index (0=accent, 1=success, 2=warning, 3=error, 4=muted).

Accessibility

  • The wrapper is a <section> with data-cb-edu="third-party-widget" and data-cb-viz reflecting the current viz mode.
  • The viz SVG / bar is aria-hidden — the legend and per-script list are the announced surface, both rendered as role="list".
  • When title is set, the root wires aria-labelledby to the heading; the totals block is wired to aria-describedby regardless.
  • Reduced-motion users get static slices and bars — no enter animation, no growth.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/sdp-web-performance/ui/ThirdPartyWidget.tsx). The original was wired to a project-level usePerfContext engine, hard-coded three specific scripts, exposed per-script defer-strategy radios (eager / defer / idle / interaction), and rendered a lifecycle timeline of parse and execute phases over a hand-built flex track. This rewrite drops the engine coupling, the per-strategy controls, and the lifecycle chrome — consumers pass a flat scripts array with impactMs and category, and the primitive handles the donut, the bar, and the per-script share list.
  • Distinct from ThirdPartyAudit: that primitive is a sortable ledger with per-row blocking bars and a budget verdict; this one is an aggregate-by-category summary.