Ant Design and Styles

Here you will learn how we used styles with AntD styles and components.

We won't talk much about AntD components a lot but should mention some things about them and how apply styles to them.

You can read their official documentation and follow that information to do everything.

AntD Components

Need to notice what AntD version do we used in moment when we develop this app.

        "antd": "^5.20.0",
        "antd-img-crop": "^4.21.0",

Almost in all cases we used their components. And sometimes they acts really interesting and hard. Especially when we talk about table filters. Why them? In process of development we realized that the filters for the table and the same filters that should be elsewhere could not be produced by a single component. Perhaps this is a mistake in the development itself, and perhaps a misunderstanding between the developers of this project and the developers of AntD.

Also AntD do not give us an flexible input for the phone. Because of that you need to use third-party libraries for this. Accordingly, this component sometimes does not fit well with AntD components.

Back to the table filters. Here is an example of filters from the Leads page.

Leads.js
filterDropdown: ({ filters, close }) =>
                !mobileDevicesBreakpoint && (
                    <TableFilterDropdown
                        filters={filters}
                        close={close}
                        filterKey="status"
                        selectedFilters={selectedFilters}
                        setSelectedFilters={setSelectedFilters}
                        setFilteredInfo={setFilteredInfo}
                    />
                ),

AntD provides us with information how to work with these filters, but if you want to put them in another place, or even develop an option for mobile applications, then everything needs to be done through states.

It would be more logical to make one state or two and control the filters with them, but it does not work. As a result, we see a bunch of states on the same page that need to control this.

Leads.js
const [filteredInfo, setFilteredInfo] = useState({});
const [selectedFilterValue, setSelectedFilterValue] = useState({});
const [selectedFilters, setSelectedFilters] = useState({
        states: [],
        statuses: [],
        businesses: [],
    });
const filterOptions = {
        state: leadState.map(state => ({
            text: state.label,
            value: state.value,
        })),
        status: statuses.map(status => ({
            text: status.name,
            value: status.id,
        })),
        business: [
            {
                text: 'No business',
                value: 'No business',
            },
            ...businesses.map(business => ({
                text: business.name,
                value: business.name,
            })),
        ],
    };

Or, for example, resetting the form.

Leads.js
setTimeout(() => {
                form.resetFields();
}, 1000);

Something needs to be done using the setTimeout function. Again, for some it may be obvious things, but for some it is not. All the same, this should be mentioned since these are that certain features in our application.

More examples. Adding or reducing the number of inputs for social medias. Just look at how difficult everything is here (sorry that you need to flip through so much, a very large nesting):

leadForm.js
<Form.Item label={t('socials')}>
	<Form.List name="socialMedia" initialvalue={[{ type: 'facebook', url: '' }]}>
		{(fields, { add, remove }) => {
			return (
			<>
			{fields.map(({ key, name, fieldKey, ...restField }, index) => {
				const initialvalue = lead?.socialMedia && lead?.socialMedia.length > index
							? lead.socialMedia[index].url
							: form.getFieldValue(['socialMedia', index, 'type']);

				return (
					<Flex key={key} align="center" style={{ width: isSmallDevice && '100%' }}>
						<Form.Item {...restField} style={{ marginBottom: spacing[4], width: isSmallDevice && '100%' }}>
							<Flex vertical={isSmallDevice} align="center" gap={spacing[1]}>
								<Form.Item {...restField} name={[socialsList[index], 'type']} noStyle>
									<Flex align="center" justify="space-between" style={{ width: isSmallDevice && '100%' }}>
										<Select
											filterOption={filterOption}
											style={{
												height: spacing[7],
												width: 82,
											}}
											options={socialMediaOptions}
											defaultValue={
												socialsList[index] || form.getFieldValue(['socialMedia', index, 'type'])
											}
											onChange={e => {
											handleSocialsListChange(index, e);
											}}
												optionRender={option => {
													if (option.data.disabled) {
														return <div style={{ opacity: 0.5 }}>{option.data.label}</div>;
													}
													
													return option.data.label;
											}}
										/>
											{index === 0 ? null : isSmallDevice && (
												<DeleteButton
													onDelete={() => {
														handleRemoveSocialMediaField(remove, name);
													}}
												/>
											)}
									</Flex>
								</Form.Item>
								<Form.Item
									{...restField}
									name={[name, 'url']}
									fieldKey={[fieldKey, 'url']}
									noStyle
									rules={[
										getSocialMediaValidationRules(
										form.getFieldValue(['socialMedia', index, 'type']) || 'facebook', t),
									]}
								>
									<Input
										placeholder={t('placeholder.socials')}
										style={{ width: isSmallDevice ? '100%' : 278 }}
										initialvalue={mode === 'edit' ? initialvalue : ''}
									/>
								</Form.Item>
									{index === 0 ? null : !isSmallDevice && (
										<DeleteButton
											onDelete={() => {
												handleRemoveSocialMediaField(remove, name);
											}}
										/>
									)}
							</Flex>
						</Form.Item>
					</Flex>
				);
			})}

			{Object.keys(socialsList).length < 4 && (
				<Button
					onClick={() => handleAddSocialMediaField(add)}
					className={classes['add-phone-button']}
					icon={<i className={`fi-rr-add ${classes['add-phone-button-icon']}`} />}
				>
					{t('buttons.add-social')}
				</Button>
			)}
			</>
		);
		}}
	</Form.List>
</Form.Item>

And the functions that regulate the number of inputs for social networks:

const handleAddSocialMediaField = () => {
	const keys = Object.keys(socialsList).map(Number);
	const maxKey = keys.length > 0 ? Math.max(...keys) : 0;

	const defaultSocialValue = getDefaultSocialSelectValue(socialsList);

	handleSocialsListChange(maxKey + 1, defaultSocialValue);
	form.setFieldValue(['socialMedia', maxKey + 1, 'type'], defaultSocialValue);
	setSocialsList({ ...socialsList, [maxKey + 1]: defaultSocialValue });
};

const handleRemoveSocialMediaField = (removeFunc, name) => {
	const { [name]: removed, ...remainingSocialsList } = socialsList;

	const newSocialsList = Object.fromEntries(
		Object.entries(remainingSocialsList).map(([key, value], index) => [index, value])
	);

	setSocialsList(newSocialsList);
	setSocialMediaOptions(prev =>
		prev.map(option => {
			if (option.value === removed) {
				return {
					...option,
					disabled: false,
				};
			}

			return option;
		})
	);
	removeFunc(name);
};

It is difficult to understand all this the first time. In the code of our project you may find a lot of examples. This may be the hardest one. But this is only half the problem.

Styling

Here comes another problem. Components rarely give the opportunity to change their stylization. Some small part can be stylized using JavaScript, the rest using ordinary styles using SCSS. Other words, use the usual className's or id.

SCSS

This is where this problem manifests itself. Using this approach, we again cannot change the entire stylization.

It is necessary to change their classes to suit their own needs.

For example

App.scss
.ant-layout-header {
    padding: $spacing-1 $spacing-6;

    @media screen and (width <= 768px) {
        padding: $spacing-1 $spacing-3;
    }
}

It is necessary, using a browser, to find the class of the element that we want to change.

Or:

.odd-table-cell {
    .ant-table-cell-fix-left,
    .ant-table-cell-fix-right {
        background-color: $gray-25;
    }
    background-color: $gray-25;
}

Here we are adding "native" classes to our own.

And most importantly, there is such an option, the best from the point of code support:

actions-menu {
    :global {
        .ant-dropdown-menu {
            gap: $spacing-1;

            .ant-dropdown-menu-item:hover {
                background-color: $gray-50;
            }

            .ant-dropdown-menu-item:active {
                background-color: $blue-50;
            }
        }
    }
}

Use ": global" and continue to prescribe classes.

JS

Some of the styling is done by using JavaScript.

Either write as "style in JS" using all the features of the optional render, or stylize through the props provided by AntD:

<Flex
                    vertical={mobileDevicesBreakpoint}
                    gap={mobileDevicesBreakpoint ? spacing[1] : spacing[3]}
                    align="center"
                    justify="space-between"
                    style={{
                        marginBottom: mobileDevicesBreakpoint ? spacing[3] : spacing[4],
                    }}
>

It is worth noting that certain style constants are used here. They are on the path: ...ui/src/theming/

There you will find everything you need, as well as the following:

export const themeConfig = {
    token,
    components,
};

Config file for AntD theme. It is used in App.js.

Look in components.js. You will find there how to customize certain styles for components. Here is part of the code from there:

export const components = {
    // Here can be defined the components' styles
    Button: {
        colorPrimary: backgroundColor.Action,
        colorPrimaryActive: backgroundColor.ActionActive,
        colorPrimaryBorder: borderColor.actionPrimary,
        colorPrimaryHover: backgroundColor.ActionHover,
        colorTextLightSolid: textColor.white,
        borderRadius: radius.md,
        defaultBg: backgroundColor.white,
        defaultHoverBg: primitiveColors.blue100,
        defaultActiveBg: primitiveColors.blue200,
        defaultBorderColor: backgroundColor.Action,
        defaultColor: textColor.brandPrimary,
        textHoverBg: primitiveColors.blue100,
        colorBgTextActive: primitiveColors.blue200,
        colorText: textColor.brandPrimary,
        colorTextDisabled: primitiveColors.gray200,
        borderColorDisabled: 'transparent',
        colorBgContainerDisabled: primitiveColors.gray50,
        colorError: backgroundColor.error,
        colorErrorBg: backgroundColor.error,
        colorErrorBgActive: primitiveColors.red700,
        colorErrorHover: primitiveColors.red700,
        colorErrorBorderHover: primitiveColors.red600,
        colorErrorActive: backgroundColor.errorActive,
        colorLink: primitiveColors.gray200,
    },
}

N.B. By using the AntD guideline, you can adjust the stylization for yourself and your needs. It's also worth remembering that styling an app requires flexibility with which styles you should use. Use all approaches to achieve maximum results, or just one for greater code purity.

Icons

It is also worth considering which icons we use. This is Flaticon. Regular, rounded. Use search inside Flaticon documentation to find for what icon you need.

Locally all icons are in .../ui/src/assets/icons/

Usage:

<i className={`fi fi-rr-edit ${icon.md}`} style={{ color: textColor.subtle }} />

This example mean "edit" icon. We can understand it by the class name.

N.B. This is important to have all the icons inside assets. Official documents says to use regular class instead always write className.

Architecture and results

  • SCSS is located in the folder of each page and component, and there is also one common to all - App.scss and index.scss. Overall SCSS styles modules are in .../ui/src/common/styles/

  • SCSS is overwhelmingly used as modules.

  • JS constants are in... ui/src/theming/- if possible, use them.

  • Styling through props of AntD components - this will help keep the code clean.

Last updated