
Contents
If you’ve built a HubSpot UI Extension — a custom card or app surface inside the CRM — you know the gap. HubSpot gives you a solid set of primitives: Table, Form, Tile, Tag. What it doesn’t give you is the layer most real cards actually need: a sortable, filterable, paginated data table with inline editing. A config-driven form with validation and multi-step flow. A Kanban board. So every team rebuilds those from the primitives, on every project, slightly differently each time.
hs-uix closes that gap, and it’s worth knowing about if you build on HubSpot. Credit where it’s due: it’s an open-source library by Carter McKay (@05bmckay on GitHub), MIT licensed. The project lives at github.com/05bmckay/hs-uix.
What it is
In the project’s own words: “Production-ready UI components for HubSpot UI Extensions. Built entirely on HubSpot’s native primitives — no custom HTML, no CSS, no iframes.”
That last clause is the important one, and it’s why this library is worth a closer look rather than rolling your own component kit.
Why “no HTML, no CSS, no iframes” actually matters
HubSpot UI Extensions run inside HubSpot’s React runtime, not a free-form web page. You compose their components; you don’t get a <div> and a stylesheet. Libraries that fight this by smuggling in custom markup or an iframe pay for it: broken theming when HubSpot updates its design system, accessibility regressions, sluggish iframe boundaries, and review friction if you’re heading for the marketplace.
Building on the platform’s primitives instead of around them is the difference between a card that ages well and one that breaks at the next HubSpot release.
hs-uix takes the disciplined path — everything is assembled from HubSpot’s own components, so it inherits HubSpot’s look, dark mode, and accessibility for free, and keeps working when the host updates. Same reason I build themes on platform primitives instead of dragging in a framework: fewer moving parts you don’t control.
Installing it
It’s on npm. React 18+ and the HubSpot UI Extensions SDK are peer dependencies (you already have both in a UI Extensions project):
npm install hs-uix
Every piece is split by entry point, so you import only what a card uses instead of pulling the whole kit.
The five pieces, in real code
1. DataTable
The one you’ll reach for constantly. You hand it a column config and rows; it gives you sorting, search, and pagination. renderCell lets each column delegate to a common component or a formatter:
import { DataTable } from "hs-uix/datatable";
import { AutoStatusTag, AutoTag } from "hs-uix/common-components";
import { formatCurrency, formatDate } from "hs-uix/utils";
const COLUMNS = [
{ field: "name", label: "Company", sortable: true, renderCell: (v) => v },
{ field: "status", label: "Status", renderCell: (v) => <AutoStatusTag value={v} /> },
{ field: "segment", label: "Segment", renderCell: (v) => <AutoTag value={v} /> },
{ field: "amount", label: "Amount", sortable: true, renderCell: (v) => formatCurrency(v) },
{ field: "closeDate", label: "Close Date", renderCell: (v) => formatDate(v) },
];
<DataTable data={deals} columns={COLUMNS} searchFields={["name"]} pageSize={10} />
2. FormBuilder
Declarative forms — describe the fields, get validation and layout. No hand-wired state, no per-field onChange plumbing:
import { FormBuilder } from "hs-uix/form";
const fields = [
{ name: "firstName", type: "text", label: "First name", required: true },
{ name: "lastName", type: "text", label: "Last name", required: true },
{ name: "email", type: "text", label: "Email",
pattern: /^[^\s@]+@[^\s@]+$/, patternMessage: "Enter a valid email" },
];
<FormBuilder
columns={2}
fields={fields}
onSubmit={(values) => console.log(values)}
/>
3. Kanban
Stage-based boards for pipeline-style views on a record. Stages are config, card layout is config, and stage changes come back as a callback you wire to your update logic:
import { Kanban } from "hs-uix/kanban";
import { AutoTag } from "hs-uix/common-components";
import { formatCurrencyCompact, formatDate } from "hs-uix/utils";
const STAGES = [
{ value: "qualified", label: "Qualified", variant: "info" },
{ value: "proposal", label: "Proposal", variant: "info" },
{ value: "negotiation", label: "Negotiation", variant: "warning" },
{ value: "closed_won", label: "Closed Won", variant: "success", terminal: true },
{ value: "closed_lost", label: "Closed Lost", variant: "default", terminal: true },
];
const CARD_FIELDS = [
{ field: "name", placement: "title" },
{ field: "company", placement: "subtitle" },
{ field: "amount", placement: "meta", render: (v) => formatCurrencyCompact(v) },
{ field: "segment", placement: "body", render: (v) => <AutoTag value={v} /> },
{ field: "closeDate", placement: "footer", render: (v) => formatDate(v) },
];
<Kanban
data={deals}
stages={STAGES}
groupBy="stage"
cardFields={CARD_FIELDS}
onStageChange={(row, stage) => updateDealStage(row.id, stage)}
/>
4. Common components
The small pieces that make a card read like HubSpot built it. AutoStatusTag / AutoTag infer the right color variant from the value, so you stop hand-mapping “At risk” → orange in every project:
import {
AutoStatusTag, AutoTag, AvatarStack, SectionHeader, KeyValueList,
} from "hs-uix/common-components";
import { formatCurrency } from "hs-uix/utils";
<SectionHeader
title="Deal Summary"
description="A compact summary block using common components."
/>
<KeyValueList
items={[
{ label: "Status", value: <AutoStatusTag value="At risk" /> },
{ label: "Segment", value: <AutoTag value="Enterprise" /> },
{ label: "Owners", value: <AvatarStack items={["AR","JK","SP","MB","LM"]} maxVisible={4} /> },
{ label: "Pipeline", value: formatCurrency(245000) },
]}
/>
5. Utils
The unglamorous functions every card re-implements: currency, dates, percentages, option builders, and the variant inference the auto-tags use. Worth importing for these alone:
import {
formatCurrency, formatCurrencyCompact, formatDate, formatPercentage,
buildOptions, findOptionLabel, getAutoTagVariant, sumBy,
} from "hs-uix/utils";
formatCurrency(1234.56); // → "$1,235"
formatCurrencyCompact(123_580_000); // → "$123.6M"
formatDate("2026-04-15"); // → "Apr 15, 2026"
formatPercentage(0.1567); // → "16%"
const statusOptions = buildOptions(
[{ name: "Open", id: "o" }, { name: "Closed", id: "c" }],
{ labelKey: "name", valueKey: "id" },
);
findOptionLabel(statusOptions, "o"); // → "Open"
getAutoTagVariant("At risk"); // → "warning"
sumBy(deals, "amount"); // → total
When to reach for it — and when not
Reach for it when you’re building real, data-heavy UI Extensions and you’d otherwise be re-implementing tables and forms for the third time. The leverage is enormous and the platform-native approach means low long-term maintenance.
Skip it when your card is genuinely simple — a couple of Text and Tag primitives don’t need a library. Adding a dependency to render two fields is the same bloat mistake in a different costume. The discipline cuts both ways: use the library when it removes real work, not reflexively.
Bottom line
If you’re doing serious HubSpot UI Extension work, hs-uix is the component layer HubSpot doesn’t ship but most cards need — and it does it the right way, on native primitives. Thanks to Carter McKay for building it and putting it out under MIT. The source, per-component docs, and examples are at github.com/05bmckay/hs-uix, and the package is on npm.