Creating Components
This guide walks you through creating new Astro components for the FiNAN website, following the established patterns and conventions.
When to Create a Component
Section titled “When to Create a Component”Create a new component when:
- Content repeats across multiple pages (navigation, footers, cards)
- Data structure is reusable (committee members, FAQs, partners)
- Functionality is self-contained (galleries, accordions, forms)
- Styling should be consistent (buttons, sections, layouts)
Don’t create a component if:
- Content appears only once and won’t be reused
- Logic is page-specific and tightly coupled to one route
- A simpler solution exists (plain HTML in page file)
Component Template
Section titled “Component Template”Use this template as a starting point for new components:
---
// 1. Import types and utilitiesimport type { ImageMetadata } from 'astro';
// 2. Define Props interfaceinterface Props { title: string; description?: string; image?: ImageMetadata; variant?: 'primary' | 'secondary';}
// 3. Destructure props with defaultsconst { title, description, image, variant = 'primary'} = Astro.props;---
<!-- 4. Component markup --><section class={`example-component ${variant}`}> <div class="container"> <h2>{title}</h2> {description && <p>{description}</p>} {image && <img src={image.src} alt={title} />} <slot /> </div></section>
<!-- 5. Scoped styles --><style> .example-component { padding: 4rem 0; }
.container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
h2 { font-size: 2rem; margin-bottom: 1rem; }
/* Variant styles */ .primary { background-color: var(--color-primary); }
.secondary { background-color: var(--color-secondary); }</style>Step-by-Step Component Creation
Section titled “Step-by-Step Component Creation”-
Create the component file
Navigate to
src/components/and create a new.astrofile with a descriptive PascalCase name:Terminal window # Example: Creating a testimonials componenttouch src/components/Testimonials.astro -
Define TypeScript props interface
Start by defining the Props interface based on what data the component needs:
interface Props {title: string; // Required propsubtitle?: string; // Optional propitems: TestimonialItem[]; // Array of typed objects} -
Import required dependencies
Add imports at the top of the component:
// Type importsimport type { ImageMetadata } from 'astro';import type { Testimonial } from '@/data/testimonialsData';// Component imports (if needed)import Icon from './Icon.astro';// Utility imports (if needed)import { formatDate } from '@/lib/utils'; -
Destructure props with default values
Extract props from
Astro.propsand provide defaults for optional props:const {title,subtitle = '', // Default empty stringitems = [], // Default empty arrayvariant = 'primary'} = Astro.props; -
Add component logic (if needed)
Process data or calculate values before rendering:
// Example: Filter and sort itemsconst activeItems = items.filter((item) => item.isActive).sort((a, b) => a.order - b.order);// Example: Derive computed valuesconst itemCount = activeItems.length;const hasItems = itemCount > 0; -
Write the component markup
Create the HTML structure using Astro’s template syntax:
<section class="testimonials"><div class="container"><h2>{title}</h2>{subtitle && <p class="subtitle">{subtitle}</p>}{hasItems ? (<div class="grid">{activeItems.map((item) => (<div class="testimonial-card"><p class="quote">{item.quote}</p><p class="author">{item.author}</p></div>))}</div>) : (<p class="empty-state">No testimonials available.</p>)}</div></section> -
Add scoped styles
Style the component using Astro’s scoped
<style>block:<style>.testimonials {padding: 4rem 0;background-color: var(--color-background);}.container {max-width: 1200px;margin: 0 auto;padding: 0 1rem;}.grid {display: grid;grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));gap: 2rem;}.testimonial-card {padding: 2rem;border-radius: 0.5rem;background-color: white;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);}</style> -
Test the component
Import and use the component in a page:
src/pages/testimonials.astro ---import Testimonials from '@/components/Testimonials.astro';import { testimonials } from '@/data/testimonialsData';---<Testimonialstitle="What People Say"subtitle="Testimonials from our community"items={testimonials}/> -
Run build validation
Ensure the component builds without errors:
Terminal window pnpm buildFix any TypeScript or build errors before committing.
TypeScript Props Best Practices
Section titled “TypeScript Props Best Practices”Required vs Optional Props
Section titled “Required vs Optional Props”Use required props for essential data and optional props for customization:
interface Props { // Required - Component won't work without these title: string; items: Item[];
// Optional - Has sensible defaults variant?: 'primary' | 'secondary'; limit?: number; showEmpty?: boolean;}Union Types for Variants
Section titled “Union Types for Variants”Use TypeScript union types to constrain prop values:
interface Props { // ✅ Good - Limited to specific values size?: 'small' | 'medium' | 'large'; theme?: 'light' | 'dark';
// ❌ Bad - Any string allowed size?: string;}Array Props with Types
Section titled “Array Props with Types”Always type array props using imported interfaces:
import type { Partner } from '@/data/partnersData';
interface Props { // ✅ Good - Type-safe array partners: Partner[];
// ❌ Bad - No type safety partners: any[];}Image Props
Section titled “Image Props”Use Astro’s ImageMetadata type for image props:
import type { ImageMetadata } from 'astro';
interface Props { // ✅ Good - Type-safe image imports logo: ImageMetadata; heroImage?: ImageMetadata;
// ❌ Bad - String paths aren't type-safe logo: string;}Styling Conventions
Section titled “Styling Conventions”Scoped Styles
Section titled “Scoped Styles”Always use scoped styles to prevent CSS conflicts:
<style> /* Scoped to this component */ .component { padding: 2rem; }
/* Child elements */ .component h2 { font-size: 2rem; }</style>CSS Variables
Section titled “CSS Variables”Use CSS custom properties for consistent theming:
<style> .component { /* Use existing CSS variables */ background-color: var(--color-primary); color: var(--color-text); padding: var(--spacing-large); }</style>Common variables in the FiNAN website:
--color-primary- Primary brand color--color-secondary- Secondary brand color--color-text- Default text color--color-background- Background color--spacing-small,--spacing-medium,--spacing-large- Consistent spacing
Responsive Design
Section titled “Responsive Design”Use mobile-first responsive design:
<style> /* Mobile styles (default) */ .grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
/* Tablet and up */ @media (min-width: 768px) { .grid { grid-template-columns: repeat(2, 1fr); gap: 2rem; } }
/* Desktop and up */ @media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }</style>Avoid Global Styles
Section titled “Avoid Global Styles”Only use :global() when absolutely necessary:
<style> /* ✅ Preferred - Scoped */ .button { padding: 1rem; }
/* ❌ Avoid - Global scope */ :global(.button) { padding: 1rem; }</style>Component Patterns
Section titled “Component Patterns”Data-Driven Component
Section titled “Data-Driven Component”Component that accepts configuration data:
---import type { Partner } from '@/data/partnersData';
interface Props { partners: Partner[]; title?: string;}
const { partners, title = 'Our Partners' } = Astro.props;---
<section class="partners"> <h2>{title}</h2> <div class="grid"> {partners.map((partner) => ( <a href={partner.url} target="_blank" rel="noopener noreferrer"> <img src={partner.logo.src} alt={partner.name} /> </a> ))} </div></section>---import Partners from '@/components/Partners.astro';import { partners } from '@/data/partnersData';---
<Partners partners={partners} title="Strategic Partners" />Layout Component with Slot
Section titled “Layout Component with Slot”Component that wraps other content:
---interface Props { title: string; background?: 'light' | 'dark';}
const { title, background = 'light' } = Astro.props;---
<section class={`section ${background}`}> <div class="container"> <h2>{title}</h2> <slot /> </div></section>
<style> .section { padding: 4rem 0; }
.light { background-color: white; }
.dark { background-color: #1a1a1a; color: white; }</style><Section title="About Us" background="dark"> <p>Custom content goes here</p> <button>Learn More</button></Section>Conditional Rendering Component
Section titled “Conditional Rendering Component”Component with conditional logic:
---interface Props { items: Item[]; emptyMessage?: string; limit?: number;}
const { items, emptyMessage = 'No items found', limit} = Astro.props;
const displayItems = limit ? items.slice(0, limit) : items;const hasItems = displayItems.length > 0;---
{hasItems ? ( <ul class="list"> {displayItems.map((item) => ( <li>{item.name}</li> ))} </ul>) : ( <p class="empty-state">{emptyMessage}</p>)}Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”1. Missing Props Interface
Section titled “1. Missing Props Interface”// ❌ Bad - No type safetyconst { title, description } = Astro.props;
// ✅ Good - Type-safe propsinterface Props { title: string; description: string;}
const { title, description } = Astro.props;2. Hardcoding Data
Section titled “2. Hardcoding Data”// ❌ Bad - Hardcoded data<div> <p>Partner 1</p> <p>Partner 2</p></div>
// ✅ Good - Data from props{partners.map((partner) => ( <p>{partner.name}</p>))}3. Unscoped Styles
Section titled “3. Unscoped Styles”<!-- ❌ Bad - Affects all .button elements --><style> .button { padding: 1rem; }</style>
<!-- ✅ Good - Scoped to component --><style> .my-component-button { padding: 1rem; }</style>4. Missing Alt Text
Section titled “4. Missing Alt Text”<!-- ❌ Bad - No alt text --><img src={image.src} />
<!-- ✅ Good - Descriptive alt text --><img src={image.src} alt={`${member.name} - ${member.role}`} />5. Not Using Optional Chaining
Section titled “5. Not Using Optional Chaining”// ❌ Bad - Will error if items is undefinedconst count = items.length;
// ✅ Good - Safe accessconst count = items?.length ?? 0;Component Checklist
Section titled “Component Checklist”Before committing a new component, verify:
- Props interface defined with TypeScript
- Required props are clearly marked (no
?) - Optional props have sensible defaults
- Styles are scoped (not
:global()) - Images have alt text
- Responsive design implemented (mobile-first)
- Conditional rendering handles edge cases
- Component builds without errors (
pnpm build) - Component tested in at least one page
- Code follows existing patterns from similar components
Examples from the Codebase
Section titled “Examples from the Codebase”Study these well-implemented components from the main repository:
- Committee.astro - Data-driven component with image handling
- FAQAccordion.astro - Interactive component with client-side logic
- Partners.astro - Simple grid layout with responsive design
- Navbar.astro - Layout component with navigation logic
- Footer.astro - Layout component with structured content
Location: https://github.com/poncardasm/finan-website/tree/main/src/components
Next Steps
Section titled “Next Steps”- Component API Reference - Documentation for all 25 existing components
- Component Architecture - Understanding component types and patterns
- Data Configuration - Creating data files for components