Patterns: Styling
Less architecture, design tokens, utility classes, BEM component styles, inline-style rules, and the
useTheme()bridge. Load via thepatterns-stylingcontext group.
Architecture
All styles are global (no CSS Modules, no styled-components). Less files compile into a single src/styles/css/main.css bundle. The React app imports only that compiled CSS in App.jsx.
src/styles/less/
main.less # Single entry point, imports everything
antd-overrides.less # Global Ant Design component overrides
abstracts/
variables.less # All Less vars + CSS custom properties
mixins.less # Reusable mixins
utils.less # Bootstrap-inspired utility classes (~1030 lines)
animations.less # Keyframes + animation classes
components/ # One .less file per UI domain
layout.less, livechat.less, forms.less, tables.less, ...
Load order in main.less:
antd/dist/antd.less(base Ant Design styles)- Third-party CSS (boxicons, react-quill-new, chatscope, reactflow)
- Fonts
- Abstracts: variables > mixins > utils > animations
antd-overrides.less- All component .less files
Less file naming
New component styles go in src/styles/less/components/<name>.less:
- Filename: lowercase, no separators (match existing convention). Examples:
livechat.less,botpreview.less,celltypography.less. Plurals are fine when the file covers a collection (alerts.less,buttons.less,cards.less). - Scope: one file per UI domain or shared component family. Do not split per-component.
- Import: add
@import 'components/<name>';to the// Componentssection ofsrc/styles/less/main.less. The section is roughly alphabetical; insert your line to keep it that way. - Abstracts vs components: tokens, mixins, utilities, and animations belong in
src/styles/less/abstracts/. Component-scoped BEM rules go incomponents/.
Design Tokens (variables.less)
All tokens are defined as @kebab-case Less variables in abstracts/variables.less. Custom brand tokens use an @hbf- prefix.
| Category | Examples | Notes |
|---|---|---|
| Brand | @primary-color, @primary-color-alt, @secondary-color | Primary: #605DEC |
| Semantic | @warning-color, @success-color, @error-color | |
| HBF custom | @hbf-light-grey-color, @hbf-muted-color, @hbf-highlight-color | |
| Lab colors | @org-color, @bot-color, @livechat-color, @observatory-color | Each has -hover and palette variants |
| Spacing | @spacer: 4px (base unit), @padding-xss/xs/sm/md/lg (from antd) | All spacing derives from @spacer |
| Layout | @layout-header-height: 64px, @layout-footer-height: 70px | |
| Shadows | @box-shadow-sm/base/lg/soft/bottom/top | All use @shadow-color |
| Typography | @font-size-xs (10px) through @font-size-xxxxl (40px) | 8 size stops |
| Font weights | @font-weight-thin (100) through @font-weight-black (900) | |
| Borders | @border-radius-base: 8px, @border-width-base: 2px, @border-color-base |
CSS Custom Properties (:root block at the bottom of variables.less) bridge Less variables to JavaScript. They use --kebab-case with no prefix:
:root {
--primary-color: @primary-color;
--text-color: @text-color;
--padding-md: @padding-md;
--font-size-lg: @font-size-lg;
--border-radius-base: @border-radius-base;
// ... ~25 properties total
}
These are consumed in JS via the useTheme() hook.
Utility Classes (utils.less)
A comprehensive Bootstrap-inspired utility system. All utilities use !important.
Generated via Less loops:
| Category | Pattern | Range | Unit |
|---|---|---|---|
| Spacing | .p-{n}, .m-{n}, .px-{n}, .py-{n}, .mx-{n}, .gap-{n} | 0-18 | n * 4px |
| Sizing | .w-{n}, .h-{n}, .min-w-{n}, .max-w-{n} | 0-100 (step 10) | percent |
| Opacity | .opacity-{n} | 0-100 (step 10) | |
| Font weight | .fw-{n} | 100-900 (step 100) | |
| Line clamp | .line-clamp-{n} | 0-10 |
Static classes (selected):
| Category | Examples |
|---|---|
| Position | .position-relative, .position-absolute, .top-0, .left-0 |
| Display | .d-none, .d-block, .d-flex, .d-inline-flex, .d-contents |
| Z-index | .z-index-0, .z-index-modal, .z-index-tooltip, .z-index-dropdown |
| Color | .text-primary, .text-success, .text-warning, .text-error, .text-muted |
| Background | .bg-primary, .bg-success, .bg-body, .bg-layout, .bg-white, .bg-active |
| Flex | .flex-row, .flex-column, .flex-fill, .flex-grow-1, .justify-content-between, .align-items-center |
| Overflow | .overflow-auto, .overflow-hidden, .overflow-y-auto |
| Text | .text-center, .text-truncate, .white-space-nowrap, .fs-sm, .fs-lg, .fw-bold |
| Borders | .bordered, .border-0, .border-bottom, .border-radius-base, .border-color-primary |
| Shadows | .shadow-sm, .shadow, .shadow-lg, .shadow-soft, .shadow-none |
| Cursor | .cursor-pointer, .cursor-not-allowed, .cursor-move |
| Scrollbar | .scrollbar-custom, .scrollbar-custom-nohide, .scrollbar-hidden |
| Animation | .animate-fade-in, .animate-spin, .opacity-transition |
How to Apply Styles (3 Mechanisms)
1. Utility classes via className (primary, preferred)
<div className="d-flex align-items-center gap-2 px-4">
<Avatar className="bg-layout cursor-pointer" />
<Typography.Text className="fw-bold fs-lg text-truncate">
{name}
</Typography.Text>
</div>
2. BEM-style with baseClassName + classnames (for component-scoped styles)
Each component that needs custom styles defines a baseClassName constant at the top:
import classNames from 'classnames';
const baseClassName = 'my-component';
const MyComponent = ({ isActive, className }) => (
<div className={classNames(baseClassName, className, {
[`${baseClassName}--active`]: isActive,
})}>
<div className={`${baseClassName}__header`}>...</div>
<div className={`${baseClassName}__body`}>...</div>
</div>
);
The corresponding Less file (in styles/less/components/) uses BEM nesting:
.my-component {
padding: @padding-md;
&__header {
font-weight: @font-weight-bold;
}
&__body {
flex: 1;
}
&--active {
border-color: @primary-color;
}
}
For classnames library usage, conditional class logic, and anti-patterns, see classnames.md.
3. Inline style={{}} (reference by category)
Below is a categorization of existing inline-style patterns, what is acceptable, and what should be migrated to CSS when touched.
Category A: Dynamic / Responsive (always acceptable)
Values computed from props, state, breakpoints, or runtime measurements. These must be inline.
// Responsive breakpoint
style={{ width: isMobile ? '100%' : `${panelWidth}px` }}
// Theme token for chart/canvas
const { primaryColor } = useTheme();
style={{ color: primaryColor }}
// Library-required style spreading (react-beautiful-dnd)
style={{ ...provided.draggableProps.style }}
// Ant Design bodyStyle with dynamic height
bodyStyle={{ height: livechatPanelHeight, display: 'flex' }}
// Form prop passthrough
style={{ maxWidth: formProps.maxInputWidth }}
Category B: Dimension Constraints (acceptable for one-offs)
Static maxHeight, maxWidth, minWidth, minHeight, height, width on individual components. These are acceptable when:
- The value is specific to one component instance (not repeated across files)
- The component is an Ant Design element that needs size constraints (Modal, Select, Collapse, RichTextEditor, scrollable panels)
- The value controls a scrollable container's bounds
// OK: one-off modal width cap
style={{ maxWidth: 600 }}
// OK: scrollable panel height constraint
style={{ maxHeight: 300, overflowY: 'auto' }}
// OK: input/select width constraint
style={{ width: 100 }}
// OK: flex min-width fix (prevents flex children from overflowing)
style={{ minWidth: 0 }}
Common acceptable shapes:
maxWidthcaps on ModalsmaxHeighton scrollable panels/dropdownsminWidth: 0/minHeight: 0flex overflow fixesminHeighton layout containerswidthon Select/InputNumber components- Ant Design
bodyStylefor Card internals (padding,height,overflow) headStylefor Card header styling
Category C: Repeated Statics (should be CSS classes)
When the same static value appears on 3+ elements, extract to a CSS class. Put the class in the relevant components/<name>.less file and reference it by className. Do not document the specific magic numbers here; they change.
Current inventory of violations is tracked in the hbf-console style migration task.
Category D: Hardcoded Colors (should use tokens)
Avoid hardcoded hex/rgba/named colors in style={{}}. Use utility classes (.text-success, .text-error, .bg-primary) or useTheme() variables instead.
Acceptable: Colors from useTheme() (primaryColor, borderColorBase, successColor, etc.) passed to chart configs or dynamic icon coloring.
Category E: Positioning Coordinates (acceptable with className pattern)
The standard pattern is: className="position-absolute" sets the positioning context, style={{ top, right }} sets the exact coordinates. This is acceptable because coordinates are typically unique per instance.
<Button className="position-absolute" style={{ top: 12, right: 20 }}>
Category F: Connector/Decorative Elements (acceptable)
Components that draw tree connector lines or geometric decorations between fields may use inline styles. These are geometric drawings with unique pixel values per line segment. Acceptable as-is.
Category G: Third-Party Library Requirements (always acceptable)
Some libraries require style props and cannot use className:
- react-beautiful-dnd:
...provided.draggableProps.stylespread (required) - ECharts:
style={{ height: N }}on wrapper (library needs explicit pixel height) - JsonView: style spread for rendered nodes
- Ant Design Skeleton:
style={{ width }}for sizing (no className support for width)
Decision Tree: Inline Style vs CSS
- Is the value computed at runtime from props/state? Inline style.
- Is it required by a third-party library API? Inline style.
- Is it a one-off dimension constraint on a specific component? Inline style (acceptable).
- Is it the same static value repeated 3+ times? Extract to CSS class.
- Is it a color/border/visual property that could use a token? Use utility class or Less variable.
- Does it use
position-absolute+ coordinates? Inline style for coordinates (acceptable).
useTheme() Hook
Located at src/utils/hooks/useTheme/index.js. Reads CSS custom properties from document.documentElement and exposes them as camelCase JS values.
Use for: chart configs, dynamic layout calculations, any JS-driven rendering that needs design tokens.
const { primaryColor, borderColorBase, layoutHeaderHeight } = useTheme();
Ant Design Customization
- Theme variables: Overridden in
variables.lessby redefining antd's@variables (e.g.,@primary-color,@border-radius-base) - Component overrides: Global in
antd-overrides.less, organized by#regioncomments - Custom modifier classNames: Defined in
antd-overrides.lessfor antd-specific patterns (e.g.,.ant-modal-p-0,.ant-collapse-p-0) - No
ConfigProvidertheme usage (antd v4 Less-based theming only)