HubSpot CRM

hs-uix: The Component Layer HubSpot UI Extensions Are Missing

· April 15, 2026 · 5 min read
hs-uix: The Component Layer HubSpot UI Extensions Are Missing
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.

← Back to Resources