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.
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: 12for tables (MANDATORY — seereferences/component-patterns.md) - Include proper TypeScript types
- Handle loading states (Skeleton, Spin, or loading prop)
- Show feedback via
App.useApp()— never the staticmessage/notification/Modalimports (see 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
ConfigProviderbecause they are rendered to independent DOM nodes. To resolve style issues, use hooks likemessage.useMessage,notification.useNotification,Modal.useModal, or theApp.useApphook. — antd FAQ
we recommend to use top level registration instead of
notificationstatic method, because static method cannot consume context, and ConfigProvider data will not work. — antd notification docs
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:
import { ConfigProvider, App } from "antd";
export default function Providers({ children }) {
return (
<ConfigProvider theme={{ token: { colorPrimary: "#F79400" } }}>
<App>{children}</App>
</ConfigProvider>
);
}
Usage (every component)
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
<Button danger
</>
);
}
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:
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
ConfigProviderfor table theme customization - Set
fontSize: 12for 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:
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} footer={null} maskClosable={true}>
<Form form={form} layout="vertical"
<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 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 staticmessageimport ignoresConfigProvider(Feedback APIs) - Set
maskClosable={true}for better UX
Pattern 3: Selectable Card Grids
Horizontal scrolling cards with selection:
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}
=> 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.colorBorderfor unselected state
Pattern 4: Loading States
Always show loading feedback:
// 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
{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
- Ant Design props first (type, size, danger, etc.)
- Theme tokens for colors (
token.colorBgContainer) - Inline styles for layout and spacing
- 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
<Tag color={status.color || "#d9d9d9"} style={{ borderRadius: "8px", fontSize: 12 }}>
{status.name || "Not Set"}
</Tag>
Clickable Text
<Text strong style={{ cursor: 'pointer', fontSize: 12 }} => router.push(`/path/${id}`)}>
{title}
</Text>
Overdue Indicator
{isOverdue && (
<Tooltip title={`Overdue since ${date}`}>
<div style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#ff4d4f' }} />
</Tooltip>
)}
Select with Item Counts
<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:
- design-tokens.md - Colors, spacing, typography system. Canonical for every color value.
- codebase-patterns.md - Real patterns from existing code
- component-patterns.md - Ant Design component standards
- 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.
- animations.md - Loading indicators and transitions. Drawer/modal behaviour is owned by
component-patterns.md; that file wins on any drawer conflict.
Typography Scale
// 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:
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:
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 — 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.