~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted Atom work.
Atom
- A single purpose component
- Deliver display or interactiveness with single action
- Foundation of reusable UI systems
Key Insight
Atoms are the fundamental building blocks of your UI system—think of them as LEGO bricks that do one thing exceptionally well. By keeping atoms focused on a single responsibility (displaying text, capturing input, showing an icon), you create highly reusable components that compose effortlessly into complex interfaces while maintaining consistency across your entire application.
Detailed Description
In the context of Atomic Design, atoms represent the smallest functional units of your user interface. Just as chemical atoms combine to form molecules, UI atoms combine to create more complex components. An atom should encapsulate a single piece of functionality or presentation—a button, an input field, a label, or an icon—nothing more.
The power of atoms lies in their simplicity and reusability. When you build an atom, you’re establishing a contract: this component does one thing, accepts specific props, and behaves predictably. This predictability is crucial for the Universal Frontend Architecture approach, where components must work seamlessly across different frameworks and contexts. A well-designed atom can be used hundreds of times throughout your application without modification.
Atoms serve as the foundation for your design system, enforcing visual and functional consistency. When your Button atom defines padding, font size, and hover states, every button across your application inherits these properties automatically. This centralization eliminates the “slightly different button” problem that plagues many codebases and makes design updates as simple as modifying a single file.
From a performance perspective, atoms are inherently optimizable. Because they’re small and focused, they’re easy to memoize, lazy-load, and tree-shake. Framework-specific optimizations like React.memo or Vue’s functional components work best with simple, pure components—exactly what atoms should be.
In the Universal Frontend Architecture, atoms bridge the gap between design and implementation. They translate design tokens (colors, spacing, typography) into interactive components, creating a shared vocabulary between designers and developers. This alignment accelerates development and ensures design fidelity across all user touchpoints.
Code Examples
Basic Example: Button Atom across frameworks
Here’s the same single-purpose Button atom pulled directly from each chota-* template. The shape is intentionally identical across all four: one prop-driven button that swaps to a Loader while isLoading is true, and emits a click otherwise. Compare how each framework expresses the same pattern.
React
// templates/chota-react-redux/src/ui/atoms/Button/Button.component.jsx
import Loader from "../Loader/Loader.component";
import './Button.style.css';
export default function Button(props) {
const transformedProps = { ...props };
delete transformedProps.isLoading;
if (props.isLoading) {
return (
<button {...transformedProps} className={`${props.className} loading-button`}>
<Loader width="2px" size="1.2rem" color="#fff" />
</button>
);
}
return <button {...transformedProps}>{props.children}</button>;
}
Angular
// templates/chota-angular-ngrx/src/ui/atoms/Button/Button.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import LoaderComponent from '../Loader/Loader.component';
@Component({
selector: 'app-button',
standalone: true,
imports: [LoaderComponent],
templateUrl: './Button.component.html',
styleUrls: ['./Button.style.css'],
})
export default class ButtonComponent {
@Input() isLoading = false;
@Input() classes?: string;
@Input() type = '';
get computedClasses() {
return this.isLoading ? `${this.classes} loading-button` : this.classes;
}
@Output() onClick = new EventEmitter<Event>();
}
<!-- Button.component.html -->
@if (isLoading) {
<button [class]="computedClasses" [disabled]="isLoading">
<app-loader width="2px" size="1.2rem" color="#fff"></app-loader>
</button>
} @else {
<button [type]="type || 'button'" (click)="onClick.emit($event)" [class]="computedClasses">
<ng-content></ng-content>
</button>
}
Vue
<!-- templates/chota-vue-pinia/src/ui/atoms/Button/Button.component.vue -->
<template>
<button v-if="isLoading" :disabled="disabled" @click="$emit('onClick')" :class="getButtonClass()">
<Loader width="2px" size="1.2rem" color="#fff" />
</button>
<button v-else :disabled="disabled" :type="type" @click="$emit('onClick')" :class="getButtonClass()">
<slot />
</button>
</template>
<script>
import { defineComponent } from 'vue'
import Loader from '../Loader/Loader.component.vue';
export default defineComponent({
components: { Loader },
props: ['isLoading', 'class', 'disabled', 'onClick', 'label', 'type'],
methods: {
getButtonClass() {
return `${this.class} loading-button`;
},
},
})
</script>
<style scoped src="./Button.style.css"></style>
Web Components
// templates/chota-wc-saga/src/ui/atoms/Button/Button.component.js
import { html } from "lit";
import style from './Button.style';
import "../Loader/app-loader";
import useComputedStyles from "../../../utils/theme/hooks/useComputedStyles";
import emit from "../../../utils/events/emit";
export default function Button(props) {
useComputedStyles(this, [style]);
if (props.isLoading) {
return html`
<button type="button" class=${`${props.classes} loading-button`}>
<app-loader .width=${'2px'} .size=${'1.2rem'} .color=${'#fff'}>Loading...</app-loader>
</button>
`;
}
return html`
<button type="button"
class="${props.classes}"
@click="${() => emit(this, "onClick", props)}">
<slot></slot>
</button>
`;
}
A few things worth comparing across the tabs:
- Children projection. React uses
{props.children}, Angular uses<ng-content>, Vue and Web Components both use<slot>— three spellings of the same idea across four frameworks. - Event shape. React passes
onClickthrough as a plain prop. Angular exposes an@Output() EventEmitter. Vue emits'onClick'via$emit. The Lit-based WC uses a tinyemit(this, "onClick", props)helper wrappingCustomEvent. - Styling. Every template co-locates
Button.style.cssnext to the component; Angular and Vue scope it, React imports it side-effect style, WC injects viauseComputedStyles.
Advanced Example: Icon Atom with a Path Lookup Table
A small icon atom showing how a single component can render any one of N variants from a static lookup, while keeping the prop surface tiny:
// Icon.js
import React from "react";
const ICON_PATHS = {
search: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
user: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
close: "M6 18L18 6M6 6l12 12",
check: "M5 13l4 4L19 7"
};
const Icon = ({
name,
size = 24,
color = "currentColor",
className = "",
ariaLabel
}) => {
const pathData = ICON_PATHS[name];
if (!pathData) {
console.warn(`Icon "${name}" not found`);
return null;
}
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={`icon ${className}`}
role="img"
aria-label={ariaLabel || `${name} icon`}
>
<path d={pathData} />
</svg>
);
};
export default Icon;
Usage demonstrating composition:
// SearchButton.js (Molecule composed of atoms)
import React from "react";
import Button from "./Button";
import Icon from "./Icon";
const SearchButton = ({ onClick }) => {
return (
<Button onClick={onClick} label="">
<Icon name="search" size={20} ariaLabel="Search" />
<span>Search</span>
</Button>
);
};
Common Mistakes
1. Making Atoms Too Complex
Mistake: Adding business logic, API calls, or state management to atom components.
// ❌ BAD: Atom with too much responsibility
const Button = ({ userId }) => {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await fetch(`/api/users/${userId}/activate`);
setLoading(false);
};
return <button onClick={handleClick}>{loading ? 'Loading...' : 'Activate'}</button>;
};
// ✅ GOOD: Atom focused on presentation
const Button = ({ onClick, label, loading = false, disabled = false }) => {
return (
<button onClick={onClick} disabled={disabled || loading}>
{loading ? 'Loading...' : label}
</button>
);
};
Why it matters: Complex atoms become difficult to reuse, test, and maintain. Keep business logic in containers or parent components.
2. Inconsistent Prop Interfaces
Mistake: Using different prop names or patterns for similar atoms.
// ❌ BAD: Inconsistent naming
const Button = ({ clickHandler, text }) => { /* ... */ };
const Input = ({ onChange, value }) => { /* ... */ };
const Checkbox = ({ onUpdate, checked }) => { /* ... */ };
// ✅ GOOD: Consistent prop patterns
const Button = ({ onClick, label, ...props }) => { /* ... */ };
const Input = ({ onChange, value, ...props }) => { /* ... */ };
const Checkbox = ({ onChange, checked, ...props }) => { /* ... */ };
Why it matters: Consistent interfaces reduce cognitive load and make atoms predictable across your codebase.
3. Ignoring Accessibility
Mistake: Building atoms without ARIA labels, keyboard support, or semantic HTML.
// ❌ BAD: Not accessible
const IconButton = ({ onClick, icon }) => (
<div onClick={onClick}>
<Icon name={icon} />
</div>
);
// ✅ GOOD: Accessible implementation
const IconButton = ({ onClick, icon, ariaLabel }) => (
<button
onClick={onClick}
aria-label={ariaLabel}
type="button"
>
<Icon name={icon} aria-hidden="true" />
</button>
);
Why it matters: Atoms are used throughout your application. Accessibility issues in atoms multiply across every instance.
Quick Quiz
References
- Atoms — Atomic Design (Brad Frost)
- Atomic Design overview — where atoms sit relative to molecules, organisms, and templates
- Molecule — the next composition layer up, where atoms start combining
- Container — where the business logic and data-fetching that atoms must not contain actually lives
- React.memo and Vue functional components — framework-level optimizations that pair naturally with pure atoms