Skip to main content

Patterns: Styling

Less architecture, design tokens, utility classes, BEM component styles, inline-style rules, and the useTheme() bridge. Load via the patterns-styling context 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:

  1. antd/dist/antd.less (base Ant Design styles)
  2. Third-party CSS (boxicons, react-quill-new, chatscope, reactflow)
  3. Fonts
  4. Abstracts: variables > mixins > utils > animations
  5. antd-overrides.less
  6. 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 // Components section of src/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 in components/.

Design Tokens (variables.less)

All tokens are defined as @kebab-case Less variables in abstracts/variables.less. Custom brand tokens use an @hbf- prefix.

CategoryExamplesNotes
Brand@primary-color, @primary-color-alt, @secondary-colorPrimary: #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-colorEach 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/topAll 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:

CategoryPatternRangeUnit
Spacing.p-{n}, .m-{n}, .px-{n}, .py-{n}, .mx-{n}, .gap-{n}0-18n * 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):

CategoryExamples
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:

  • maxWidth caps on Modals
  • maxHeight on scrollable panels/dropdowns
  • minWidth: 0 / minHeight: 0 flex overflow fixes
  • minHeight on layout containers
  • width on Select/InputNumber components
  • Ant Design bodyStyle for Card internals (padding, height, overflow)
  • headStyle for 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.style spread (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

  1. Is the value computed at runtime from props/state? Inline style.
  2. Is it required by a third-party library API? Inline style.
  3. Is it a one-off dimension constraint on a specific component? Inline style (acceptable).
  4. Is it the same static value repeated 3+ times? Extract to CSS class.
  5. Is it a color/border/visual property that could use a token? Use utility class or Less variable.
  6. 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.less by redefining antd's @ variables (e.g., @primary-color, @border-radius-base)
  • Component overrides: Global in antd-overrides.less, organized by #region comments
  • Custom modifier classNames: Defined in antd-overrides.less for antd-specific patterns (e.g., .ant-modal-p-0, .ant-collapse-p-0)
  • No ConfigProvider theme usage (antd v4 Less-based theming only)