Coding style guide
The Earth Design System sticks to certain coding styles that prioritise following standards, code readability and reuse that is followed within the aw-frontend-monorepo
.
To save time and effort in defining coding styles and ensuring we align with them, we utilise Prettier and ESLint to do a lot the heavy lifting.
The aw-frontend-monorepo
has sensible defaults and strictness in linting rules that improve code readability but doesn't get in the way. We recommend using the same configurations in your own projects, not just for consistency but also for developer efficiency.
Not all coding styles can be linted however - the below covers other coding styles we aim to adhere to in EDS.
Prefer EDS components
When building a component, whether for EDS or custom for your project, always use EDS components as building blocks where possible.
EDS components come built in with defaults that stick to the design language of the design system, so should always be used for consistency.
// DON’T
<div className="flex flex-col gap-3">
<img src="https://www.example.com/borico-profile.png" />
<h3 className="text-heading-14 line-clamp-2">Borico Jones</h3>
<div className="text-body-12 text-secondary line-clamp-1">Hair stylist</div>
</div>
// DO
<Stack className="gap-3">
<Avatar size="64">
<AvatarImage src="https://www.example.com/borico-profile.png" />
</Avatar>
<Heading as="h3" className="text-heading-14 line-clamp-2">
Borico Jones
</Heading>
<Text className="text-body-12 text-secondary line-clamp-1">
Hair stylist
</Text>
</Stack>
Native HTML elements or non-EDS custom components should only be used where there is no EDS equivalent, or plans to build one, or in other exception cases such as components required extreme performance requirements.
Prop naming
To ensure EDS remains easy to use for developers, consistent prop naming is critically important.
Always refer to other components to see how certain props have been named and defined, and use the same naming where possible.
When naming booleans, start with words like “is”, “has”, or “should”. This makes is easy for consumers to understand that it's a boolean.
Avoid using negated booleans like notEnabled
, which create ambiguity regarding the actual state represented by the variable, requiring readers to mentally negate the name to understand its meaning. Instead use language that’s effectively equivalent like isDisabled
to aid code comprehension.
// DON’T
type SomeProps = {
disabled: boolean;
monthPicker: boolean;
usePortal: boolean;
};
// DO
type SomeProps = {
isDisabled: boolean;
hasMonthPicker: boolean;
shouldUsePortal: boolean;
};
Handlers and callbacks should be prefixed with "on" to define intent and aid code comprehension. It's also important to provide function signatures for clarity and type safety.
// DON’T
type SomeProps = {
clickHandler: Function;
redirect: Function;
};
// DO
type SomeProps = {
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
onRedirect: (url: string) => boolean;
};
Rename variables during destructuring (if needed) rather than create a new variable, only to give it a different name. For example, starting with a capital letter so that it can be used as a React component.
// DON’T
function SomeComponent({ icon, isSelected }) {
const Icon = icon;
const isActuallySelected = condition ? isSelected : false;
...
}
// DO
function SomeComponent({ icon: Icon, isSelected: isSelectedProp }) {
const isSelected = condition ? isSelectedProp : false;
...
}
Adding JSDoc comments above your prop types lets users access inline documentation conveniently. When users hover over or navigate through the autocomplete menu, these comments provide valuable insights into the purpose and usage of the types.
type SomeProps = {
/**
* Whether the component can be focused or interacted with. */
* @default false
*/
isDisabled?: boolean;
/**
* Handler that is called when the component is clicked.
*/
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
/**
* Callback that is called when there's a redirect attempt.
* @returns A boolean indicating whether the redirect was successful.
*/
onRedirect: (url: string) => boolean;
};
Utilise TypeScript string literal unions for props with a limited set of values. Avoid enums as they aren’t stripped out like other TypeScript constructs and instead emit code, adding unnecessary runtime overhead.
// DON'T
enum Size {
SMALL = 'small',
STANDARD = 'standard',
LARGE = 'large'
}
// DO
type Size = 'small' | 'standard' | 'large';
Stick to design tokens
When styling components, always use Tailwind classes as well as the custom classes added to EDS.
EDS adds custom classes for colors and typography.
Avoid inline styles and custom CSS unless absolutely necessary.
// DON’T
<div className="w-[256px] bg-[#ffffff]" style={{ height: "256px", fontSize: "14px" }}>
Avoid inline styles or Tailwind's arbitrary classes.
</div>
// DO
<div className="bg-canvas body-text-14 h-64 w-64">
Use Tailwind's classes for spacing, and EDS classes for colors and typography.
</div>
Write semantic markup
When you need to reach for native HTML, always write semantic markup.
Semantic markup provides meaning and structure to the content of a webpage. By using semantic HTML elements developers can convey the intended purpose and hierarchy of different parts of the page to both browsers and assistive technologies like screen readers.
This not only improves accessibility for users with disabilities but also enhances search engine optimisation (SEO) by helping search engines better understand the content and context of the page.
// DON’T
<div>
<img src="..." />
<div>Section title</div>
<div>Section body content</div>
</div>
// DO
<section>
<img src="..." alt="descriptive alt text for screen readers" />
<h2>Section title</h2>
<p>Section body content</p>
</section>
Semantic HTML elements come with inherent functionality that can greatly simplify your code. For instance, a button element automatically becomes focusable, can be activated using the keyboard, and can trigger click events, without the need for additional JavaScript.
// DON’T
<span onClick={someHandler} />
// DO
<button onClick={someHandler} />
// DON’T
<button onClick={() => window.location = '/some/page'} />
// DO
<a href="/some/page" />
It is crucial to properly label elements for assistive technologies. Proper labelling can include alternative text for images, captioning for videos, and clear attribution for hierarchical and interactive elements.
// DON’T
<button>
<InfoIcon />
</button>
// DO
<button aria-label="Information">
<InfoIcon />
</button>
Taking these measures will significantly enhance the user experience for individuals who rely on assistive technologies.
Use named exports
Using named exports exclusively in modern JavaScript has become a popular practice for several reasons:
- Clarity and Explicitness: Named exports make it clear what functionality or variables are being exported from a module. Developers can easily see all the exports at the top of the file, improving code readability and maintainability.
- Avoids Ambiguity: Default exports can sometimes lead to ambiguity when importing modules, especially when dealing with multiple exports from a single module. Named exports eliminate this ambiguity by requiring each exported item to have a specific name.
- Dead Code Elimination (DCE): When using a bundler like Webpack or Rollup, named exports allow for more efficient tree-shaking, a process where unused code is removed from the final bundle. This is because the bundler can easily determine which exports are being used based on their explicit names.
- Consistency: By consistently using named exports across your project, you maintain a consistent pattern for importing and exporting modules, which can make the codebase more cohesive and easier to understand for both current and future developers.
- Encourages Modularity: Named exports promote a modular approach to coding, where each module has a clear interface defined by its named exports. This can lead to more maintainable and reusable code.
Overall, while default exports still have their place in certain scenarios, named exports have become the preferred choice in modern JavaScript development due to their clarity, predictability, and compatibility with tools like bundlers and linters.
Be thoughtful with comments
Comments should explain the functionality of complex code segments or provide context for future reference. They should be concise, clear, and directly related to the code they are annotating. Comments can be particularly useful in explaining the 'why' behind a piece of code, especially when the code's functionality might not be immediately apparent from reading it.
// DON’T
// Set x to 10
const x = 10;
// DO
// Using a Set here instead of an Array or Object for efficient checks.
// This code runs frequently, and performance is critical.
const allowedProperties = new Set(["name", "age", "city", "country"]);
Care should be taken not to leave unnecessary commented-out code within the project. Commented-out code can cause confusion. If a piece of code is not currently being used, it should be removed from the codebase.
Avoid magic numbers
"Magic numbers" are unnamed numerical constants that appear directly in source code. They can make code difficult to understand and maintain, as they often lack context or explanation. Whenever you use a number in your code that has a specific meaning, consider defining it as a named constant instead.
// DON’T
function calculateDiscount(price) {
return price - price * 0.1;
}
// DO
const DISCOUNT_RATE = 0.1;
function calculateDiscount(price) {
return price - price * DISCOUNT_RATE;
}
Document instances in your code where the use of magic numbers is necessary but should ideally be avoided. You should still assign the value to a variable for clarity and ease of maintenance.
const inputStyles = ({ theme }) => ({
height: theme.spacing(6),
paddingInline: 'var(--input-horizontal-gutter)',
'--input-horizontal-gutter': '14px', // this value isn't exposed by Material theme
});
Branching logic
Avoid if
or switch
statements to map values. Where possible prefer objects for a cleaner and more concise syntax, reducing the complexity of conditional statements.
// DON’T
function SomeComponent({ severity }) {
let icon;
if (severity === 'error') {
icon = <ErrorIcon />;
} else if (severity === 'info') {
icon = <InformationIcon />;
} else if (severity === 'success') {
icon = <CheckCircleIcon />;
} else if (severity === 'warning') {
icon = <WarningIcon />;
}
...
}
// DO
const ICON_MAP = {
error: <ErrorIcon />,
info: <InformationIcon />,
success: <CheckCircleIcon />,
warning: <WarningIcon />,
};
function SomeComponent({ severity }) {
const icon = ICON_MAP[severity];
...
}
Early exit to improve performance by reducing unnecessary computation and enhance code readability by making the control flow more explicit.
// DON’T
function calculateDiscount(price: number, discount = 0) {
return price - price * (discount / 100);
}
// DO
function calculateDiscount(price: number, discount?: number) {
if (!discount) {
return price;
}
return price - price * (discount / 100);
}
Code hygiene
Some more general tips to keeping good code hygiene:
- Prefer composition or React context over prop drilling, especially when passing data through many layers. Prop drilling can make components tightly coupled and harder to maintain. Composition keeps components flexible, while context is ideal for shared state or config.
- Break up large, complex components into smaller parts to improve readability and maintainability, e.g. use separate files for styles, logic, types, and JSX.
- Don’t over-abstract - only create hooks or wrappers when logic is truly shared across multiple components.
- Avoid premature optimization - prioritize clear, readable code first, optimize for performance only when needed.
- Only expose internal state, hooks, or types that consumers actually need. Adding exports later is easy (minor version), removing them is hard (major version).