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.jsonUsage
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 thecb-labelstyle) and adescriptionsub-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
- Aggregation. On every render the component groups
scriptsbycategoryinto a stable ordered list. Category order comes from thecategoriesprop first (insertion order), then falls back to first-seen order inscripts. NegativeimpactMsvalues clamp to zero so the donut math stays sane. - Tone cycling. Each category gets one of five token tones — accent, success, warning, error, muted — by index. Pass
categories[].toneto pin a category to a specific slot. The same tone is reused on the slice, the legend dot, and the per-script row. - Donut. The slices are rendered as a single SVG with stacked
circlestrokes — each slice gets astrokeDasharraysized to its share and astrokeDashoffsetthat advances the cursor around the ring. The total reads through the centre. - Bar. The stacked bar lays each category as a
flexsegment whose width is its share of the total. The bar grows from zero on mount withSPRINGS.smooth; reduced-motion users see it instantly. - Per-script rows. Each row enters once with
SPRINGS.snapand a tinyyshift. The category dot and the percentage share recolour to match the category's tone slot.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
scripts | ThirdPartyWidgetScript[] | required | Scripts to summarise. |
title | ReactNode | — | Optional heading above the panel. |
description | ReactNode | — | Optional sub-headline under the title. |
categories | ThirdPartyWidgetCategory[] | — | Override category order, labels, and tone slots. |
viz | 'donut' | 'bar' | 'donut' | Visualisation style. |
unit | string | 'ms' | Unit suffix on the totals readout. |
totalLabel | ReactNode | 'Total impact' | Label rendered next to the total. |
hideScriptList | boolean | false | Hide the per-script list. |
hideLegend | boolean | false | Hide the legend chips. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
ThirdPartyWidgetScript
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier, used as React key. |
label | ReactNode | Human-readable script label. |
impactMs | number | Main-thread impact in milliseconds. |
category | string | Category bucket — matches a categories[].id. |
ThirdPartyWidgetCategory
| Field | Type | Description |
|---|---|---|
id | string | Matches script.category. |
label | ReactNode | Display label for the legend. Defaults to id. |
tone | number | Tone slot index (0=accent, 1=success, 2=warning, 3=error, 4=muted). |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="third-party-widget"anddata-cb-vizreflecting the current viz mode. - The viz SVG / bar is
aria-hidden— the legend and per-script list are the announced surface, both rendered asrole="list". - When
titleis set, the root wiresaria-labelledbyto the heading; the totals block is wired toaria-describedbyregardless. - 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-levelusePerfContextengine, 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 flatscriptsarray withimpactMsandcategory, 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.