---
name: Product UI Taste
slug: product-ui-taste
category: Frontend
description: Product UI Taste guides dense product surfaces like dashboards, tables, forms, and wizards. Use it for app UIs that must handle real data, edge cases, accessibility, and established design systems.
github: "https://github.com/huytieu/COG-second-brain/tree/main/skills/product-ui-taste"
language: HTML
stars: 930
forks: 110
install: "npx degit https://github.com/huytieu/COG-second-brain/tree/main/skills/product-ui-taste ~/.claude/skills/product-ui-taste"
installs_to: ~/.claude/skills/product-ui-taste
source_path: skills/product-ui-taste/SKILL.md
collection_size: 25
category_size: 567
collection_url: "https://dirskills.com/collections/huytieu/COG-second-brain"
added: 2026-08-22T05:20:37.673Z
last_synced: 2026-08-22T05:20:37.673Z
canonical_url: "https://dirskills.com/skills/product-ui-taste"
---

# Product UI Taste

Product UI Taste guides dense product surfaces like dashboards, tables, forms, and wizards. Use it for app UIs that must handle real data, edge cases, accessibility, and established design systems.

**Install:**

```bash
npx degit https://github.com/huytieu/COG-second-brain/tree/main/skills/product-ui-taste ~/.claude/skills/product-ui-taste
```

## README

# product-ui-taste: Anti-Slop Skill for Dense Product Surfaces

> Dashboards, data tables, forms, wizards, settings, list/detail, admin consoles, app shells. NOT landing pages, portfolios, or marketing (that is `taste-skill`). NOT charts (that is `dataviz`). NOT native mobile.
> Every rule is **contextual and mechanical**. It fires from the surface you are building, not automatically. Read the surface, budget the frame, then pull only what fits.
> **Companion contract:** `taste-skill` explicitly hands off "dashboards / dense product UI / data tables / multi-step forms" to "the right tool." This skill is that tool. If a brief is half marketing and half product (a landing page with an embedded live dashboard), use `taste-skill` for the hero/marketing sections and this skill for the product surface. Never run both on the same component.

---

## 0. PRODUCT READ (Read the Surface Before Anything Else)

Marketing UI lives on first impression. Product UI lives on the **hundredth** use, under real data, by someone doing a job. The slop failure mode is different: not "templated aesthetic," but **prototype that dies on contact with real data** - a table that scrolls the whole page sideways, a button that truncates its own label, a single grey "no data" box reused for three different situations, a form that clears itself when the user fat-fingers one field.

### 0.A Read these signals first
1. **Surface type** - the single most important read. One of: **index/table** (list of records to scan, filter, select), **detail/record** (one entity, its fields and related data), **dashboard/overview** (KPIs + widgets + a table or two), **form/settings** (input and configuration), **multi-step flow/wizard** (a sequential task), **console/observability** (logs, metrics, deploys), **feed/messaging** (stream of items). Most real screens are a composition of two or three (index + detail drawer; dashboard + drill-in table).
2. **Data shape and volume** - how many rows at p95, how wide is a row, are cells uniform or ragged, is the data live/streaming or static, can a field be empty/null/very-long.
3. **Density need** - a trader's cockpit and a consumer settings page are both "product UI" and want opposite densities. Read: expert-daily-driver (compact, keyboard-first) vs. occasional-consumer (comfortable, forgiving).
4. **Consequence level** - is the primary action informational (view), reversible (rename, move), or destructive/irreversible (delete, deploy, charge a card). This drives confirmation, focus defaults, and autosave eligibility.
5. **Who is the user and what are they permitted to do** - roles, read-only viewers, plan/license tiers. Product UI has states marketing UI never has: permission-denied, read-only, plan-locked (Section 6.G). Read this now, not after building the happy path.
6. **Existing system** - is there already a Carbon/Fluent/Polaris/Atlaskit app, or a house token set? Product UI is almost never greenfield. Match the host system before importing taste.

### 0.B Output a one-line "Product Read" before generating
Before any code, state: **"Reading this as: a \<surface type> for \<user> at \<density>, \<data volume>, consequence \<level>, built on \<design system | house tokens | none>."**

Examples:
- *"Reading this as: an index+detail-drawer for internal ops at compact density, ~2k rows (virtualize), consequence reversible, built on the house system (Table + side-panel inspector)."*
- *"Reading this as: a settings/form surface for end customers at comfortable density, low volume, consequence mixed (billing = destructive), built on the house system's form layout."*

### 0.C The anti-default that matters most: rows, not cards
The single fastest way to make a product screen look like an AI prototype is to **wrap every record in a Card with a Badge**. Dense data that the user scans, filters, sorts, or selects belongs in **rows** (Table for columnar, List/Item for single-line), edge-to-edge, with dividers and 32-40px row height. Card is a **widget container** (KPI tile, chart panel, settings group, gallery entry), never a list-item wrapper. [Material3 lists; Carbon] If your first instinct on a list of records is a Card grid, stop and re-read this line.

### 0.D If the brief is ambiguous, ask ONE question, then proceed
The one question worth asking is almost always about **data volume or consequence** (they change the architecture): "roughly how many rows at the high end?" or "is this action reversible?" Everything else, infer from the Product Read and state your assumption. Do not interview.

### 0.E Anti-default discipline
The model's defaults for product UI are: Card-soup, fixed-width `<table>` that overflows the page, `disabled` submit button as the only validation, one generic empty state, `z-index: 9999`, and a full-page spinner on every load. Each has a specific correct replacement below. When you catch yourself reaching for one, that is the signal to open the matching section.

---

## 1. THE THREE DIALS (Core Configuration)

After the Product Read, set three dials. Unlike taste-skill (VARIANCE / MOTION / DENSITY tuned for expressive marketing), product dials are tuned for **information work**. Motion is deliberately near-absent in product UI; it is feedback, not decoration.

* **`DENSITY: 2`** - 1 = comfortable/consumer, 2 = standard, 3 = compact/cockpit. Drives row height, padding scale, font size, and whether you offer a density switcher.
* **`DATA_COMPLEXITY: 2`** - 1 = simple list (plain Table/List), 2 = sortable+filterable+selectable table, 3 = enterprise grid (frozen columns, virtualization, column management, live data).
* **`CONSEQUENCE: 2`** - 1 = informational/read, 2 = reversible mutations, 3 = destructive/irreversible/financial. Drives confirmation defaults, autosave eligibility, error tone, and how loud "unsaved changes" gets.

**Baseline: `2 / 2 / 2`** (a standard SaaS product table with a detail view). Override from the Product Read.

### 1.A Dial inference
| Signal | DENSITY | DATA_COMPLEXITY | CONSEQUENCE |
|---|---|---|---|
| "internal admin / ops tool / expert daily driver" | 3 | 2-3 | 2 |
| "enterprise data console / grid / analytics" | 3 | 3 | 2 |
| "consumer settings / account / onboarding" | 1 | 1 | 1-2 |
| "SaaS product dashboard (default)" | 2 | 2 | 2 |
| "billing / delete / deploy / permissions surface" | match | match | 3 |
| "observability / logs / live metrics" | 3 | 3 | 1 |

### 1.B Use-case presets
| Preset | DENSITY | DATA_COMPLEXITY | CONSEQUENCE | Frame |
|---|---|---|---|---|
| Internal admin tool | 3 | 2 | 2 | AppShell + SideNav, inspector on select |
| SaaS product dashboard | 2 | 2 | 2 | AppShell + SideNav or TopNav + TabList |
| Enterprise data console | 3 | 3 | 2 | AppShell + SideNav, full-height grid |
| Consumer settings/account | 1 | 1 | 1-2 | AppShell + settings template (nav + form panels) |
| Multi-step wizard | 1 | 1 | 2-3 | Centered column, stepper, no distracting chrome |

### 1.C How the dials drive output
Use these exact names as global variables in reasoning. `DENSITY` gates row height and the density-switcher decision (Section 10.A). `DATA_COMPLEXITY` gates how far down the table deep-dive (Section 7) you go. `CONSEQUENCE` gates Sections 6.G, 8.E, 8.F, 9.A. Do not invent aliases.

---

## 2. PRODUCT ARCHETYPE → DESIGN SYSTEM MAP

Product UI is almost never hand-rolled CSS. Pick the foundation from the host app first, the archetype second. **One system per app.** Do not mix Carbon with Fluent, or drop shadcn into a Polaris tree.

### 2.A House system first (discover, do not guess)
If the app already has a design system, that system wins, and your first job is to **find out what it actually ships** rather than assuming. Most mature systems already solve the dense-surface problems this skill is about, so you rarely drop to raw CSS. Guessing at component names and props is the single most common way an agent produces code that looks right and does not compile.

Before writing UI, resolve each of these against the host system's real API (its CLI, its docs site, or its `.d.ts` files):

| Need | What to look for | How to resolve it |
|---|---|---|
| Full-page shell | An app-shell primitive plus its nav slots | System docs "layout" or "app shell" page |
| Multi-pane tool / inspector | A layout + side-panel pair, not two floated divs | Docs "layout" page; check whether the panel is resizable |
| Columnar data | The system's Table and its sort/select/pagination plugins | Component reference, not a blog example |
| Sticky / frozen columns | A dedicated hook or plugin (most systems ship one) | Search the docs for "sticky" or "frozen" before hand-rolling |
| Resizable columns | A column-resize hook | Same |
| Pagination footer | Table pagination + page-size control | Component reference |
| Search + filter toolbar | A toolbar/filter component distinct from plain search | Component reference |
| Single-line records | A List / Item pair | Component reference |
| Empty region | An EmptyState component | Component reference |
| Status / metadata | A status indicator distinct from Badge | Token/status docs |

**Workflow, always:** find the closest page or block template the system already ships, study its layout skeleton, then read the real props for every component you use. **Never invent props.** Prefer the system's own layout components over raw `<div>`s so spacing stays on the system's scale. Use design tokens for every value (`var(--color-*)`, spacing tokens), never raw hex or px. Reserve Badge for counts and enumerated states; status belongs on a status indicator.

**The house-system bridge (resolve the tension):** the CSS mechanics in Sections 5-10 (scroll ownership, `position:sticky` headers, frozen-column offsets, z-index tiers, `min-width:0`) are the **contract a mature system's hooks already satisfy** under the hood. Inside such a system, reach for the hook or plugin first, because a real sticky-column hook gives you the offset, the solid background, and the shadow divider correctly. Drop to the raw CSS contract only (a) to verify the system did it right when something bleeds through, or (b) when you are outside a design system entirely. Never hand-roll a sticky column with inline styles inside a system Table when the hook exists.

### 2.B External systems (match the host, or pick by archetype)
| Archetype / brief | Reach for | Why |
|---|---|---|
| IBM-style enterprise analytics, dense grids | `@carbon/react` + `@carbon/styles` | Most mature data-density + 2x-grid + empty/loading patterns [carbon] |
| Microsoft / enterprise SaaS, DataGrid | `@fluentui/react-components` (v9) | Official Fluent 2, Overflow primitives, tokens [fluent] |
| Atlassian / Jira-style product | `@atlaskit/*` + `@atlaskit/tokens` | Semantic spacing tokens chosen by meaning [atlaskit] |
| Merchant/admin, resource lists | Polaris (`@shopify/polaris` or web components) | IndexTable, four-states philosophy, error content [polaris] |
| GitHub-style devtool chrome | `@primer/react` / `@primer/css` | PageLayout landmarks, Blankslate, Truncate [primer] |
| Adaptive/canonical layouts, consumer-ish | `@material/web` + Material 3 tokens | Window-size classes, list-detail/supporting-pane [material3] |
| Own-the-code lean internal tool | Radix Primitives + shadcn/ui + TanStack Table | Headless a11y primitives, you own styling [radix, tanstack] |
| Enterprise React data-dense fast | Ant Design or Mantine + Mantine React Table | Batteries-included Table/Form [ant, mantine] |
| Lean keyboard-first (Linear/Vercel vibe) | Radix/shadcn + custom tokens | Density + command palette + restraint [linear, vercel] |

**Honesty rule:** if the app is one of these, use the official package and its Table/Form/Dialog. Do not recreate Carbon's data table in raw CSS. Do not import a system's tokens then override 90% of them.

---

## 3. FRAME-FIRST APP-SHELL ARCHITECTURE

**Budget the frame in pixels before writing one line of content.** Real applications are built top-down: pick the shell, name its regions, give each an explicit px budget, decide its container policy and responsive behavior, then fill. Content-first layout (write sections, wrap each in a Card) produces a padded scroll column that reads as a prototype. [most systems' layout docs]

### 3.A Pick the shell
- **AppShell** (header and/or side nav) - the default full-page product frame.
- **Layout + LayoutContent + LayoutPanel** - multi-pane tools: explorers, consoles, master-detail with an inspector.
- **Plain content column** - a single form, a settings section, a wizard.

### 3.B Region px budgets (memorize these)
| Region | Budget |
|---|---|
| Side nav | 240-280px |
| Icon rail (collapsed nav) | 64-72px |
| Detail / inspector panel | 340-420px (resizable 320-480) |
| Filter / facet rail | 220-260px |
| Row height | 32px compact / 40px standard / 48px comfortable [dense systems budget 32-40; Carbon ships 5 relative tiers, no fixed px] |
| Header / top nav | 48-64px (common app-bar convention) |

### 3.C Landmark regions (do not conflate them)
Name every region with a real landmark and know the difference [primer PageLayout]:
- **Header** - full width, top, app chrome.
- **Content** - the primary region, flexes to fill.
- **Pane** - a secondary region **beside** Content, content-height only. Use for an inspector or supporting info.
- **Sidebar** - **full-height** navigation, distinct from Pane. Never build a "sidebar" that is really a content-height pane, or a "pane" that is really full-height nav.
- **Footer** - full width, bottom (status bar, pagination context).

### 3.D Canonical layouts (recognize and scaffold) [material3]
- **List-detail** - list on the left, selected record on the right. Below ~1024px, detail overlays or becomes a separate route.
- **Supporting-pane** - primary content ~2/3, supporting pane ~1/3.
- **Feed** - a single scrolling stream (rows/bubbles, no cards in the stream).
- **Master-detail with inspector** (the tool backbone) - selecting a row opens a **fixed-width LayoutPanel inspector**, it does not navigate away. Add `resizable`. Overlay it below ~1024px instead of compressing Content.

### 3.E App-shell chrome that marketing UI never has
- **Workspace / org switcher** - almost every SaaS shell needs one, anchored top of the side nav or header-left, showing the current tenant's name/avatar. On click, a dropdown (not a modal) lists workspaces with a search field once there are 8+, a "recent workspaces" group, and a "create/join" action at the bottom. It switches context (data scope), it is not a nav destination. Keep it one element; never scatter tenant choice into the nav list. Persist the last-active workspace per user.
- **Global command palette** (Cmd/Ctrl-K) - for DATA_COMPLEXITY >= 2 keyboard-first tools, this is the fastest navigation. Recognize it as a first-class shell affordance, not a nice-to-have.
- **Background-job / async status tray** - when the app kicks off concurrent long-running operations (imports, exports, deploys), a persistent notification tray (a header icon with a count badge opening a dropdown of jobs) beats a modal per job. Each job row shows a determinate progress bar, a cancel affordance, and terminal success/error with a result link. Carbon's rule: for waits over a few minutes, let the user leave and notify on completion. Never block the whole UI on one background job. [carbon loading]

### 3.F Container policy per region (Cards vs Rows, decided per archetype)
| Archetype | Container policy |
|---|---|
| Tracker / work tool (issues, tickets, CRM) | Rows only. Grouped edge-to-edge lists, zero cards. |
| Console / observability | Card grid for dashboard widgets; Table for everything else. |
| Messaging / feed | Rows and bubbles. No cards in the stream. |
| Media / gallery | Card grid (ClickableCard) + dense metadata rows in detail. |
| Settings / forms | FormLayout sections; Card only to group dangerous/billing actions. |

Banned: a Card per list item (card soup), stacked full-width Cards as page structure, nested Cards, Badge as decoration.

---

## 4. LAYOUT HARD RULES (Failing any of these ships broken work)

* **SCROLL OWNERSHIP (the #1 product-UI layout bug).** Exactly **one** wrapper owns each scroll axis (`overflow:auto`). The page `<body>` must **never** scroll horizontally. A wide table scrolls inside its own container, not by pushing the whole app sideways. Sticky headers use `position:sticky` scoped to that scroll wrapper, **never** `position:fixed` to the viewport. [carbon, ant table-scroll, tanstack] If two nested elements both scroll the same axis, you have a bug.
* **Every sticky element needs a bounded-height ancestor.** `position:sticky` silently does nothing if no ancestor establishes a scroll boundary. A sticky table header requires the table's scroll container to have a real max-height. [css mechanics]
* **min-width:0 on flex children that hold text.** A flex/grid child defaults to `min-width:auto`, which refuses to shrink below content size, so `text-overflow:ellipsis` silently no-ops and the child pushes the layout wider instead. Any truncating text inside a flex row needs `min-width:0` on the text-holding child. This is the most common reason "my ellipsis isn't working." [overflow-truncation]
* **Responsive contract declared up front, per region.** Before filling, write which regions collapse, overlay, or drop at which breakpoints. No "Tailwind will handle it" assumptions. Nav collapses to an icon rail or off-canvas MobileNav; the inspector overlays below ~1024px; a wide table keeps horizontal scroll rather than reflowing columns.
* **Breakpoints: use size classes, not device names.** Compact / medium / expanded (Material 3) or Carbon's exact 2x-grid breakpoints (320 / 672 / 1056 / 1312 / 1584, 8px mini-unit, 16px padding, 32px gutter) as a fallback grid. [material3, carbon 2x-grid]
* **Container queries for reusable components.** A card or panel that appears in a wide Content region and a narrow Pane must respond to **its own** width, not the viewport. Use `container-type` + `@container`, not viewport media queries, for anything that renders in more than one region width. [joshwcomeau, responsive-layout]
* **Sidebar collapse = off-canvas or icon-rail, not in-place shrink.** Collapsing a 260px nav to 180px just cramps it. Collapse to a 64-72px icon rail (with tooltips) or slide it off-canvas with a persistent reopen strip. Persist which panes were shown across a resize/fold so the user does not lose context.
* **Density is a layout MODE, not a CSS zoom.** Offer at least the density presets the DENSITY dial calls for as a real switch (row height + padding token swap), not a browser-zoom hack. [i18n-a11y-density, carbon 5 row heights]
* **Never hardcode a fixed width on a text container.** Badges, buttons, table headers, chips, tags must be auto-layout (flex/grid, hug-contents) so text can reflow or expand. A fixed-width text container clips under real data and under translation (Section 10.B).
* **Tabular numbers for compared numbers.** Any column or list of numbers the user compares vertically uses `font-variant-numeric: tabular-nums` so digits align. Proportional digits in a metrics column is a Tell.

---

## 5. OVERFLOW & TRUNCATION CATALOG (Deep-Dive)

Product UI is where real strings arrive: a 90-character file path, a German button label, a customer name that is one word or forty. Truncation is a scalpel, not a default.

### 5.A The never-truncate list (mandatory)
**Never truncate primary/identifying content**: titles, entity names, unique IDs, error and validation messages, page headers, button labels. Truncation is for **secondary/supplementary** content only (descriptions, secondary metadata, breadcrumb middles). [carbon overflow, primer truncate] If an identifying string is too long, wrap it, give it more space, or use middle-truncation that preserves both ends (for paths/IDs) - never a trailing ellipsis that hides the part that distinguishes two records.

### 5.B Every truncated
