# UI Guidelines

> Ant Design house style for one specific React/Next.js application — its design tokens, color palette, typography scale, 8px/12px/16px/24px spacing, animations, and copy-ready patterns for antd tables, forms in modals, selectable card grids, loading states, avatars, and status tags. Use when building or restyling a form, table, modal, card, list, or page layout in a codebase that already standardizes on Ant Design and the new component must match the existing design system. It is not a general UI or accessibility reference, does not apply to Tailwind, Material UI, Chakra, or plain-CSS projects, and carries no chart or data-visualization guidance — charts, graphs, dashboards, and palette choice for data viz are out of scope.

- Skill: `pfangueiro/ui-guidelines` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add pfangueiro/ui-guidelines`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pfangueiro/ui-guidelines/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: pfangueiro (https://skillmd.com/u/pfangueiro)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pfangueiro/ui-guidelines

---


# UI Guidelines for Application

## Overview

This skill provides UI/UX guidelines for building components in a React/Next.js application using Ant Design and consistent design tokens. It ensures all new components match the existing design system with proper colors, spacing, typography, and interaction patterns.

## When to Use This Skill

Trigger this skill when:
- **Creating UI components**: forms, tables, modals, cards, lists
- **Adding new features** that require UI elements
- **Implementing loading states** or animations
- **Styling components** to match the design system
- **User asks to build/create/add** any visual component

## Out of Scope — Stop and Do Something Else

If the work you are about to do is any of the following, **stop using this skill** and take the redirect instead. Nothing below applies:

- **Charts, graphs, plots, or data-visualization palettes** (categorical / sequential / diverging series colors, sparklines, heatmaps, stat tiles, dashboard chart layout). This skill carries **no** chart guidance, and its brand palette is not a series palette — do not extrapolate one from it. Use a dedicated data-visualization skill or reference (`dataviz`, where available) instead.
- **A codebase that is not standardized on Ant Design** — Tailwind-only, Material UI, Chakra, or plain CSS. None of the patterns below transfer; follow that stack's own conventions.
- **A general accessibility or UX audit.** This is one application's house style, not an a11y reference.

Charts are out of scope regardless of what a reference file says — see the note on `styling-layout.md` under [Reference Files](#reference-files).

## Quick Start

### Step 1: Identify Component Type

Determine what you're building:
- **Drawer/Side Panel** → Read `references/component-patterns.md` (Drawer Patterns section) **[MANDATORY PATTERN]**
- **Data Table** → Read `references/codebase-patterns.md` (Tables section)
- **Form/Modal** → Read `references/codebase-patterns.md` (Modal Patterns section)
- **Card/Grid** → Read `references/codebase-patterns.md` (Card Patterns section)
- **Need colors/spacing** → Read `references/design-tokens.md`
- **Need animations/loading** → Read `references/animations.md`

### Step 2: Follow the Component Checklist

This is the **authoritative** checklist — use it both before you start and before you call the component done. Two reference files carry narrower, topic-scoped checklists (`styling-layout.md` for layout, `animations.md` for animation). They supplement this one; where either disagrees with this list, **this list wins**.

Every component must:
- [ ] Use Ant Design components as the base
- [ ] Apply consistent spacing (8px, 12px, 16px, 24px)
- [ ] Use theme tokens (`token.colorText`, `token.colorBgContainer`)
- [ ] Use `fontSize: 12` for tables (MANDATORY — see `references/component-patterns.md`)
- [ ] Include proper TypeScript types
- [ ] Handle loading states (Skeleton, Spin, or loading prop)
- [ ] Show feedback via `App.useApp()` — never the static `message` / `notification` / `Modal` imports (see [Feedback APIs](#feedback-apis))
- [ ] Include validation rules with clear messages on every form input
- [ ] Include proper error handling
- [ ] Support responsive design
- [ ] Match the existing patterns below

### Step 3: Apply Core Design Tokens

**Colors** (canonical source: `references/design-tokens.md` — if these ever disagree, that file wins):
- Brand Orange: `#F79400` (primary brand color)
- Product Owner: `#7C4DFF` (purple)
- Tech Owner: `#52c41a` (green)
- Error/Overdue: `#ff4d4f` (red)
- Always use `theme.useToken()` for dynamic colors

**Spacing:**
- Small gap: `8px`
- Medium gap: `12px`
- Standard padding: `16px`
- Section margin: `24px`

**Typography:**
- Table cells: `fontSize: 12`
- Secondary text: `fontSize: '11px'`
- Strong text: `<Text strong>`
- Secondary: `<Text type="secondary">`

## Feedback APIs

**MANDATORY: get `message`, `notification` and `modal` from `App.useApp()`. Never call the static `message.*` / `notification.*` / `Modal.confirm()` imports.**

This is not a style preference — it is forced by the `ConfigProvider` theming that the rest of this skill mandates. Ant Design's own docs state it plainly:

> Static methods like message, notification, and Modal.confirm do not inherit context from `ConfigProvider` because they are rendered to independent DOM nodes. To resolve style issues, use hooks like `message.useMessage`, `notification.useNotification`, `Modal.useModal`, or the `App.useApp` hook.
> — [antd FAQ](https://ant.design/docs/react/faq)

> we recommend to use top level registration instead of `notification` static method, because static method cannot consume context, and ConfigProvider data will not work.
> — [antd notification docs](https://ant.design/components/notification)

Measured on antd 5.29.3: inside `<ConfigProvider prefixCls="zzz" theme={{ token: { colorPrimary: '#F79400' } }}>`, a static `message.success()` rendered its holder with the **`ant-`** prefix and the **default** theme hash, while `App.useApp().message.success()` rendered it with the configured **`zzz-`** prefix and a *different*, configured hash. The static call silently ignored both the prefix and the brand token. (Reproduce it by diffing the two holders' class names — the exact hash strings are build-specific, so compare them against each other rather than against any literal quoted here.)

### Setup (once, at the app root)

`App` must sit **inside** `ConfigProvider` so it inherits the theme:

```tsx
import { ConfigProvider, App } from "antd";

export default function Providers({ children }) {
  return (
    <ConfigProvider theme={{ token: { colorPrimary: "#F79400" } }}>
      <App>{children}</App>
    </ConfigProvider>
  );
}
```

### Usage (every component)

```tsx
import { App, Button } from "antd";

export default function MyComponent() {
  // Call the hook at the top of the component...
  const { message, notification, modal } = App.useApp();

  // ...but FIRE the feedback from an event handler or an effect, never during render.
  // antd warns ("You are calling notice in render") and the notice is dropped.
  const handleSave = async () => {
    try {
      await save();
      message.success("Saved");
    } catch (err) {
      notification.error({ message: "Save failed", description: String(err) });
    }
  };

  const handleDelete = () => {
    modal.confirm({ title: "Are you sure?", onOk: doDelete });
  };

  return (
    <>
      <Button onClick={handleSave}>Save</Button>
      <Button danger onClick={handleDelete}>Delete</Button>
    </>
  );
}
```

| Don't (static — ignores `ConfigProvider`) | Do (`App.useApp()`) |
|---|---|
| `import { message } from "antd"; message.success(...)` | `const { message } = App.useApp(); message.success(...)` |
| `import { notification } from "antd"; notification.error(...)` | `const { notification } = App.useApp(); notification.error(...)` |
| `Modal.confirm({...})` | `const { modal } = App.useApp(); modal.confirm({...})` |

`App.useApp()` is a hook — call it at the top of the component, not inside a callback. For feedback fired outside React (an axios interceptor, a plain util), lift the call into a component or pass the instance in; do not fall back to the static import.

**Note:** `<Modal>` used as a *component* (Pattern 2 below) is unaffected — it renders inside the tree and inherits context normally. Only the static `Modal.confirm()` / `Modal.info()` family is affected.

## Core Patterns

### Pattern 1: Data Tables

All data tables should follow this structure:

```tsx
import { Table, Input, Select, ConfigProvider, theme } from "antd";
import { SearchOutlined } from "@ant-design/icons";

export default function DataTable() {
  const { token } = theme.useToken();
  
  return (
    <div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
      {/* Filters row */}
      <div style={{ marginBottom: 16, display: "flex", gap: "8px" }}>
        <Input placeholder="Search..." prefix={<SearchOutlined />} style={{ flex: 1 }} allowClear />
        <Select style={{ flex: 1 }} placeholder="Filter" allowClear />
      </div>

      {/* Table with custom theme */}
      <ConfigProvider theme={{
        components: { Table: { headerBg: token.colorBgContainer, fontSize: 12 } }
      }}>
        <Table
          dataSource={data}
          loading={isLoading}
          rowKey="id"
          size="small"
          pagination={false}
          scroll={{ y: 'calc(100vh - 220px)', x: 'max-content' }}
        />
      </ConfigProvider>
    </div>
  );
}
```

**Key details:**
- Use `ConfigProvider` for table theme customization
- Set `fontSize: 12` for compact display
- Use `scroll={{ y: 'calc(100vh - 220px)' }}` for proper scrolling
- Always include `rowKey="id"`
- Set `size="small"` for compact tables

### Pattern 2: Forms in Modals

Standard form modal pattern:

```tsx
import { Modal, Form, Input, Button, App } from "antd";

export default function AddModal({ isOpen, onClose, onSuccess }) {
  // MANDATORY: not the static `message` import — it ignores ConfigProvider.
  // See "Feedback APIs" above.
  const { message } = App.useApp();
  const [form] = Form.useForm();
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async () => {
    try {
      const values = await form.validateFields();
      setIsSubmitting(true);
      await api.post('/endpoint', values);
      message.success("Created successfully");
      form.resetFields();
      onSuccess();
      onClose();
    } catch (error) {
      message.error(error.response?.data?.error || "Failed to create");
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <Modal title="Add Item" open={isOpen} onCancel={onClose} footer={null} maskClosable={true}>
      <Form form={form} layout="vertical" onFinish={handleSubmit}>
        <Form.Item name="name" label="Name" rules={[{ required: true, message: "Please enter name" }]}>
          <Input placeholder="Enter name" />
        </Form.Item>
        
        <Form.Item style={{ marginBottom: 0, marginTop: 16, textAlign: "right" }}>
          <Button onClick={onClose} style={{ marginRight: 8 }}>Cancel</Button>
          <Button htmlType="submit" loading={isSubmitting}>Create</Button>
        </Form.Item>
      </Form>
    </Modal>
  );
}
```

**Key details:**
- Use `layout="vertical"` for labels above inputs
- Include validation rules with clear messages
- Reset form on success: `form.resetFields()`
- Show feedback via `const { message } = App.useApp()` — the static `message` import ignores `ConfigProvider` ([Feedback APIs](#feedback-apis))
- Set `maskClosable={true}` for better UX

### Pattern 3: Selectable Card Grids

Horizontal scrolling cards with selection:

```tsx
import { theme, Typography, Avatar } from "antd";

const { Text } = Typography;
const { token } = theme.useToken();

<div style={{
  display: "flex",
  gap: "12px",
  overflowX: "auto",
  paddingBottom: "4px"
}}>
  {items.map((item) => {
    const isSelected = selectedId === item.id;
    return (
      <div
        key={item.id}
        onClick={() => setSelectedId(isSelected ? null : item.id)}
        style={{
          flex: 1,
          minWidth: '200px',
          maxWidth: '280px',
          padding: '12px',
          cursor: 'pointer',
          borderRadius: '8px',
          border: isSelected ? '1px solid #F79400' : `1px solid ${token.colorBorder}`,
          backgroundColor: isSelected ? token.colorFillSecondary : token.colorBgContainer,
          boxShadow: '0 1px 2px rgba(0, 0, 0, 0.04)',
        }}
      >
        <Text strong style={{ fontSize: '14px' }}>{item.name}</Text>
        {/* Additional content */}
      </div>
    );
  })}
</div>
```

**Key details:**
- Selected state: border with brand primary color
- Subtle shadow: `0 1px 2px rgba(0, 0, 0, 0.04)`
- Border radius: `8px`
- Use `token.colorBorder` for unselected state

### Pattern 4: Loading States

Always show loading feedback:

```tsx
// Table loading
<Table loading={isLoading} dataSource={data} />

// Card loading skeleton
{data === undefined ? (
  <Card loading style={{ minWidth: '200px' }} />
) : (
  <Card>{content}</Card>
)}

// Button loading
<Button htmlType="submit" loading={isSubmitting}>Submit</Button>

// Page content skeleton
<Skeleton active paragraph={{ rows: 4 }} />
```

### Pattern 5: Avatars with Fallbacks

```tsx
{user.profilePic ? (
  <Avatar size={24} src={getCachedAvatarUrl(user.profilePic)} />
) : (
  <Avatar size={24} style={{ backgroundColor: getAvatarColor(user.name) }}>
    {user.name.charAt(0).toUpperCase()}
  </Avatar>
)}
```

## Styling Approach

### Priority Order
1. **Ant Design props** first (type, size, danger, etc.)
2. **Theme tokens** for colors (`token.colorBgContainer`)
3. **Inline styles** for layout and spacing
4. **Tailwind** rarely (only for utility classes)

### DO's ✅
- Use Ant Design components as base
- Use `theme.useToken()` for colors
- Apply consistent spacing (8px, 12px, 16px, 24px)
- Include loading states everywhere
- Show user feedback with messages
- Handle errors gracefully
- Use TypeScript types

### DON'Ts ❌
- Don't use CSS-in-JS libraries
- Don't create custom CSS files
- Don't hardcode a color that has a theme token (`token.colorText`, `token.colorBorder`, `token.colorBgContainer`) — the brand palette in Step 3 is the documented exception and is written literally, as the patterns above show
- Don't skip error handling
- Don't ignore responsive design
- Don't use excessive shadows

## Common Recipes

### Status Tag
```tsx
<Tag color={status.color || "#d9d9d9"} style={{ borderRadius: "8px", fontSize: 12 }}>
  {status.name || "Not Set"}
</Tag>
```

### Clickable Text
```tsx
<Text strong style={{ cursor: 'pointer', fontSize: 12 }} onClick={() => router.push(`/path/${id}`)}>
  {title}
</Text>
```

### Overdue Indicator
```tsx
{isOverdue && (
  <Tooltip title={`Overdue since ${date}`}>
    <div style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#ff4d4f' }} />
  </Tooltip>
)}
```

### Select with Item Counts
```tsx
<Select>
  {items.map(item => (
    <Option key={item.id} value={item.id}>
      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <span>{item.name}</span>
        <Text type="secondary" style={{ fontSize: '11px' }}>{item.count} items</Text>
      </div>
    </Option>
  ))}
</Select>
```

## Reference Files

Read these files for detailed information:

1. **[design-tokens.md](references/design-tokens.md)** - Colors, spacing, typography system. **Canonical for every color value.**
2. **[codebase-patterns.md](references/codebase-patterns.md)** - Real patterns from existing code
3. **[component-patterns.md](references/component-patterns.md)** - Ant Design component standards
4. **[styling-layout.md](references/styling-layout.md)** - Layout patterns and responsive design. Written Tailwind-first, so it reads against the grain of this file's Ant-Design-first mandates; its known conflicts (spacing grid, CSS files, inline styles, z-index, breakpoints, router, brand primary, charts) are reconciled inline in that file. Where anything there still disagrees with this file, **this file wins**.
5. **[animations.md](references/animations.md)** - Loading indicators and transitions. Drawer/modal behaviour is owned by `component-patterns.md`; that file wins on any drawer conflict.

## Typography Scale

```tsx
// Strong text (table headers, card titles)
<Text strong style={{ fontSize: '12px' }}>Title</Text>

// Regular text (table cells, body)
<Text style={{ fontSize: '12px' }}>Content</Text>

// Secondary text (subtitles, metadata)
<Text type="secondary" style={{ fontSize: '11px' }}>Metadata</Text>

// Tertiary text (additional info)
<Text style={{ fontSize: '11px', color: token.colorTextTertiary }}>Info</Text>
```

## Page Layout

Standard full-height page layout:

```tsx
export default function Page() {
  return (
    <div style={{ 
      height: "100%", 
      display: "flex", 
      flexDirection: "column",
      overflow: "hidden",
      padding: "24px 40px 24px 24px"
    }}>
      {/* Page content with proper scrolling */}
    </div>
  );
}
```

## Data Fetching

Use SWR for data fetching:

```tsx
import useSWR from "swr";
import { fetcher } from "@/lib/axios";

const { data, error, isLoading, mutate } = useSWR(
  '/api/endpoint',
  fetcher,
  {
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
  }
);
```

## Before You Finish

Re-run the checklist in [Step 2](#step-2-follow-the-component-checklist) — there is no separate finishing list to diverge from it. If you also worked from `styling-layout.md` or `animations.md`, their topic checklists are supplements to Step 2, never replacements, and Step 2 wins on any conflict.

