Molecules
What are Molecules?
Section titled “What are Molecules?”Molecules are simple groups of atoms functioning together as a unit. They take on their own properties and serve as the backbone of design systems by creating reusable, purpose-driven components.
Characteristics
Section titled “Characteristics”- Composed - Made of 2-5 atoms working together
- Single Purpose - Serve one specific function
- Reusable - Used across multiple organisms
- Self-Contained - Include all atoms needed for their purpose
- Stateless - Don’t manage complex state (usually)
Common Molecules
Section titled “Common Molecules”Search Bar
Section titled “Search Bar”---import Input from '@/components/atoms/Input.astro';import Button from '@/components/atoms/Button.astro';import Icon from '@/components/atoms/Icon.astro';
interface Props { placeholder?: string; buttonText?: string;}
const { placeholder = 'Search...', buttonText = 'Search'} = Astro.props;---
<form class="search-bar" role="search"> <Input type="text" name="query" placeholder={placeholder} /> <Button type="submit" variant="primary"> <Icon name="search" size="sm" /> <span class="button-text">{buttonText}</span> </Button></form>
<style> .search-bar { display: flex; gap: 0.5rem; max-width: 600px; }
.button-text { margin-left: 0.5rem; }
@media (max-width: 640px) { .button-text { display: none; } }</style>Form Field
Section titled “Form Field”---import Label from '@/components/atoms/Label.astro';import Input from '@/components/atoms/Input.astro';
interface Props { label: string; name: string; type?: 'text' | 'email' | 'password' | 'number'; placeholder?: string; required?: boolean; error?: string; helpText?: string;}
const { label, name, type = 'text', placeholder, required = false, error, helpText} = Astro.props;---
<div class="form-field"> <Label for={name} required={required}> {label} </Label>
<Input type={type} name={name} id={name} placeholder={placeholder} required={required} error={!!error} />
{helpText && !error && ( <p class="help-text">{helpText}</p> )}
{error && ( <p class="error-text">{error}</p> )}</div>
<style> .form-field { margin-bottom: 1rem; }
.help-text { margin-top: 0.25rem; font-size: 0.875rem; color: var(--color-text-muted); }
.error-text { margin-top: 0.25rem; font-size: 0.875rem; color: var(--color-danger); }</style>Card Header
Section titled “Card Header”---import Heading from '@/components/atoms/Heading.astro';import Icon from '@/components/atoms/Icon.astro';import Badge from '@/components/atoms/Badge.astro';
interface Props { title: string; icon?: string; badge?: string; badgeVariant?: 'success' | 'warning' | 'danger';}
const { title, icon, badge, badgeVariant } = Astro.props;---
<div class="card-header"> <div class="header-content"> {icon && <Icon name={icon} />} <Heading level={3}>{title}</Heading> </div>
{badge && ( <Badge variant={badgeVariant}> {badge} </Badge> )}</div>
<style> .card-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem; border-bottom: 1px solid var(--color-border); }
.header-content { display: flex; align-items: center; gap: 0.75rem; }</style>Navigation Item
Section titled “Navigation Item”---import Link from '@/components/atoms/Link.astro';import Icon from '@/components/atoms/Icon.astro';
interface Props { href: string; label: string; icon?: string; active?: boolean; external?: boolean;}
const { href, label, icon, active = false, external = false} = Astro.props;---
<li class="nav-item"> <Link href={href} external={external} class:list={['nav-link', { 'nav-link-active': active }]} > {icon && <Icon name={icon} size="sm" />} <span>{label}</span> </Link></li>
<style> .nav-item { list-style: none; }
.nav-link { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-radius: 0.375rem; transition: all 0.2s; }
.nav-link:hover { background: var(--color-background-hover); }
.nav-link-active { background: var(--color-primary-alpha); color: var(--color-primary); }</style>Social Link
Section titled “Social Link”---import Link from '@/components/atoms/Link.astro';import Icon from '@/components/atoms/Icon.astro';
interface Props { platform: 'github' | 'twitter' | 'linkedin' | 'youtube'; href: string; label?: string;}
const { platform, href, label } = Astro.props;---
<Link href={href} external class="social-link" aria-label={label || platform}> <Icon name={platform} /> {label && <span class="social-label">{label}</span>}</Link>
<style> .social-link { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-radius: 0.375rem; transition: all 0.2s; }
.social-link:hover { background: var(--color-background-hover); transform: translateY(-2px); }
.social-label { font-size: 0.875rem; }</style>Price Tag
Section titled “Price Tag”---import Badge from '@/components/atoms/Badge.astro';
interface Props { price: number; originalPrice?: number; currency?: string;}
const { price, originalPrice, currency = '$'} = Astro.props;
const discount = originalPrice ? Math.round(((originalPrice - price) / originalPrice) * 100) : 0;---
<div class="price-tag"> <div class="price-content"> <span class="current-price"> {currency}{price.toFixed(2)} </span>
{originalPrice && ( <span class="original-price"> {currency}{originalPrice.toFixed(2)} </span> )} </div>
{discount > 0 && ( <Badge variant="success" size="sm"> {discount}% OFF </Badge> )}</div>
<style> .price-tag { display: flex; align-items: center; gap: 0.75rem; }
.price-content { display: flex; align-items: baseline; gap: 0.5rem; }
.current-price { font-size: 1.5rem; font-weight: 700; color: var(--color-primary); }
.original-price { font-size: 1rem; color: var(--color-text-muted); text-decoration: line-through; }</style>Stat Display
Section titled “Stat Display”---import Heading from '@/components/atoms/Heading.astro';import Icon from '@/components/atoms/Icon.astro';
interface Props { label: string; value: string | number; icon?: string; trend?: 'up' | 'down'; trendValue?: string;}
const { label, value, icon, trend, trendValue } = Astro.props;---
<div class="stat-display"> {icon && ( <div class="stat-icon"> <Icon name={icon} size="lg" /> </div> )}
<div class="stat-content"> <p class="stat-label">{label}</p> <Heading level={2}>{value}</Heading>
{trend && trendValue && ( <div class:list={['stat-trend', `stat-trend-${trend}`]}> <Icon name={trend === 'up' ? 'arrow-up' : 'arrow-down'} size="sm" /> <span>{trendValue}</span> </div> )} </div></div>
<style> .stat-display { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: var(--color-background-secondary); border-radius: 0.5rem; }
.stat-icon { color: var(--color-primary); }
.stat-content { flex: 1; }
.stat-label { font-size: 0.875rem; color: var(--color-text-muted); margin-bottom: 0.25rem; }
.stat-trend { display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem; margin-top: 0.25rem; }
.stat-trend-up { color: var(--color-success); }
.stat-trend-down { color: var(--color-danger); }</style>Best Practices for Molecules
Section titled “Best Practices for Molecules”✅ Combine 2-5 atoms maximum ✅ Give molecules clear names ✅ Make molecules reusable ✅ Keep logic simple ✅ Handle their own layout ✅ Accept configuration props ✅ Document molecule usage ✅ Test molecule interactions
Don’ts
Section titled “Don’ts”❌ Make molecules too complex ❌ Include business logic ❌ Depend on page context ❌ Create one-off molecules ❌ Nest molecules too deeply ❌ Mix too many concerns ❌ Forget accessibility
Testing Molecules
Section titled “Testing Molecules”import { render, screen } from '@testing-library/svelte';import FormField from './FormField.svelte';
describe('FormField Molecule', () => { test('renders label and input', () => { render(FormField, { props: { label: 'Email', name: 'email' } });
expect(screen.getByLabelText('Email')).toBeInTheDocument(); expect(screen.getByRole('textbox')).toBeInTheDocument(); });
test('shows error message when error prop provided', () => { render(FormField, { props: { label: 'Email', name: 'email', error: 'Invalid email' } });
expect(screen.getByText('Invalid email')).toBeInTheDocument(); });
test('shows required indicator', () => { render(FormField, { props: { label: 'Email', name: 'email', required: true } });
expect(screen.getByText('*')).toBeInTheDocument(); });});Composition Patterns
Section titled “Composition Patterns”Flexible Molecules
Section titled “Flexible Molecules”Create molecules that can adapt to different contexts:
<FormField label="Email" name="email" type="email" required helpText="We'll never share your email"/>
<FormField label="Password" name="password" type="password" required error="Password must be at least 8 characters"/>Slot-Based Molecules
Section titled “Slot-Based Molecules”Allow customization through slots:
<CardHeader title="User Profile"> <slot name="actions"> <Button size="sm">Edit</Button> </slot></CardHeader>