Skip to content

Atoms

Atoms are the basic building blocks of matter applied to web interfaces. They’re the foundational HTML elements that can’t be broken down any further without losing their meaning.

  • Indivisible - Can’t be meaningfully broken down further
  • Single Purpose - Do one thing well
  • Highly Reusable - Used across many molecules and organisms
  • Abstract - Function independently of context
  • Styled - Have consistent styling defined
components/atoms/Button.astro
---
interface Props {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
}
const {
variant = 'primary',
size = 'md',
type = 'button',
disabled = false
} = Astro.props;
---
<button
type={type}
disabled={disabled}
class:list={['btn', `btn-${variant}`, `btn-${size}`]}
>
<slot />
</button>
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Sizes */
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
}
.btn-md {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
/* Variants */
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-secondary {
background: var(--color-secondary);
color: white;
}
.btn-danger {
background: var(--color-danger);
color: white;
}
.btn-ghost {
background: transparent;
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-ghost:hover:not(:disabled) {
background: var(--color-background-hover);
}
</style>
components/atoms/Input.astro
---
interface Props {
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
name: string;
placeholder?: string;
value?: string;
required?: boolean;
disabled?: boolean;
error?: boolean;
}
const {
type = 'text',
name,
placeholder,
value,
required = false,
disabled = false,
error = false
} = Astro.props;
---
<input
type={type}
name={name}
placeholder={placeholder}
value={value}
required={required}
disabled={disabled}
class:list={['input', { 'input-error': error }]}
/>
<style>
.input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
font-size: 1rem;
transition: all 0.2s;
}
.input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-alpha);
}
.input:disabled {
background: var(--color-background-disabled);
cursor: not-allowed;
}
.input-error {
border-color: var(--color-danger);
}
.input-error:focus {
border-color: var(--color-danger);
box-shadow: 0 0 0 3px var(--color-danger-alpha);
}
</style>
components/atoms/Label.astro
---
interface Props {
for?: string;
required?: boolean;
}
const { for: htmlFor, required = false } = Astro.props;
---
<label for={htmlFor} class="label">
<slot />
{required && <span class="required">*</span>}
</label>
<style>
.label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
margin-bottom: 0.25rem;
}
.required {
color: var(--color-danger);
margin-left: 0.25rem;
}
</style>
components/atoms/Icon.astro
---
interface Props {
name: string;
size?: 'sm' | 'md' | 'lg';
color?: string;
}
const { name, size = 'md', color } = Astro.props;
const sizeMap = {
sm: 16,
md: 20,
lg: 24
};
const iconSize = sizeMap[size];
---
<svg
width={iconSize}
height={iconSize}
class="icon"
style={color ? `color: ${color}` : undefined}
aria-hidden="true"
>
<use href={`/icons/sprite.svg#${name}`}></use>
</svg>
<style>
.icon {
display: inline-block;
vertical-align: middle;
fill: currentColor;
}
</style>
components/atoms/Heading.astro
---
interface Props {
level: 1 | 2 | 3 | 4 | 5 | 6;
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
}
const { level, as } = Astro.props;
const Tag = as || `h${level}`;
---
<Tag class={`heading heading-${level}`}>
<slot />
</Tag>
<style>
.heading {
font-weight: 600;
line-height: 1.2;
color: var(--color-heading);
}
.heading-1 { font-size: 2.5rem; }
.heading-2 { font-size: 2rem; }
.heading-3 { font-size: 1.75rem; }
.heading-4 { font-size: 1.5rem; }
.heading-5 { font-size: 1.25rem; }
.heading-6 { font-size: 1rem; }
</style>
components/atoms/Badge.astro
---
interface Props {
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
size?: 'sm' | 'md';
}
const { variant = 'default', size = 'md' } = Astro.props;
---
<span class:list={['badge', `badge-${variant}`, `badge-${size}`]}>
<slot />
</span>
<style>
.badge {
display: inline-flex;
align-items: center;
font-weight: 500;
border-radius: 9999px;
}
.badge-sm {
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
}
.badge-md {
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
}
.badge-default {
background: var(--color-background-secondary);
color: var(--color-text);
}
.badge-success {
background: var(--color-success-light);
color: var(--color-success-dark);
}
.badge-warning {
background: var(--color-warning-light);
color: var(--color-warning-dark);
}
.badge-danger {
background: var(--color-danger-light);
color: var(--color-danger-dark);
}
.badge-info {
background: var(--color-info-light);
color: var(--color-info-dark);
}
</style>
components/atoms/Link.astro
---
interface Props {
href: string;
variant?: 'default' | 'primary' | 'muted';
external?: boolean;
}
const { href, variant = 'default', external = false } = Astro.props;
const externalProps = external
? { target: '_blank', rel: 'noopener noreferrer' }
: {};
---
<a
href={href}
class:list={['link', `link-${variant}`]}
{...externalProps}
>
<slot />
</a>
<style>
.link {
text-decoration: none;
transition: color 0.2s;
}
.link-default {
color: var(--color-text);
text-decoration: underline;
}
.link-default:hover {
color: var(--color-primary);
}
.link-primary {
color: var(--color-primary);
}
.link-primary:hover {
color: var(--color-primary-dark);
}
.link-muted {
color: var(--color-text-muted);
}
.link-muted:hover {
color: var(--color-text);
}
</style>

✅ Keep atoms simple and focused ✅ Make atoms highly reusable ✅ Use consistent naming conventions ✅ Provide clear prop interfaces ✅ Include all necessary variants ✅ Add proper accessibility attributes ✅ Document usage examples ✅ Test atoms in isolation

❌ Add business logic to atoms ❌ Make atoms depend on context ❌ Create overly specific atoms ❌ Skip accessibility considerations ❌ Hardcode values that should be props ❌ Create atoms that are too complex ❌ Forget to handle edge cases

// Example with Testing Library
import { render, screen } from '@testing-library/svelte';
import Button from './Button.svelte';
describe('Button Atom', () => {
test('renders with text', () => {
render(Button, { props: { children: 'Click me' } });
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
test('applies variant classes', () => {
render(Button, { props: { variant: 'primary' } });
expect(screen.getByRole('button')).toHaveClass('btn-primary');
});
test('disables button when disabled prop is true', () => {
render(Button, { props: { disabled: true } });
expect(screen.getByRole('button')).toBeDisabled();
});
});

Use tools like Storybook to document atoms:

Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};