Skip to main content

Overview

Chapinismos uses Astro components (.astro files) for building the UI. Components are highly reusable, support props, and can include scoped styles and client-side scripts.

Component Architecture

Component Categories

components/
├── Layout Components      # Header, Footer, Navigation
├── Feature Components     # SearchBox, WordCard, Ticker
├── Home Components        # Homepage sections
├── Icon Components        # SVG icons
└── Schema Components      # JSON-LD structured data

Core Components

Header Component

Main site header with logo, navigation, language switcher, and theme toggle.
---
import { getLangFromUrl } from "../utils/i18n";
import Logo from "./icons/Logo.astro";
import LanguageSwitcher from "./LanguageSwitcher.astro";
import Navigation from "./Navigation.astro";
import MobileMenu from "./MobileMenu.astro";
import ThemeToggle from "./ThemeToggle.astro";

const lang = getLangFromUrl(Astro.url);
const prefix = lang === "en" ? "/en" : "/es";
---

<header class="relative mx-auto max-w-[1100px] px-4 py-4" transition:persist>
  <div class="flex items-center justify-between gap-4">
    <a href={`${prefix}/`} class="flex items-center gap-2">
      <Logo height={75} />
    </a>

    <!-- Desktop navigation -->
    <nav class="hidden items-center gap-2 md:flex">
      <Navigation />
      <LanguageSwitcher />
      <ThemeToggle />
    </nav>

    <!-- Mobile navigation -->
    <div class="flex items-center gap-2 md:hidden">
      <LanguageSwitcher />
      <ThemeToggle isMobile={true} />
      <MobileMenu />
    </div>
  </div>
</header>
None - Uses Astro.url to detect language

WordCard Component

Displays a word preview card with category badge.
---
import type { CollectionEntry } from "astro:content";
import { getCategoryColor } from "../utils/categoryColors";
import { useTranslations } from "../utils/i18n";
import { ArrowRight } from "@lucide/astro";

interface Props {
  entry: CollectionEntry<"words-es" | "words-en">;
  lang: string;
  titleLevel?: "h3" | "h4";
}

const { entry, lang, titleLevel = "h4" } = Astro.props;
const t = useTranslations(lang);
---

<a
  href={`/${lang}/palabras/${entry.slug}/`}
  class="word-related-card rounded-lg border p-4 transition-all hover:-translate-y-1"
>
  {entry.data.category && (
    <span
      class="absolute top-3 right-3 rounded-full px-2 py-0.5 text-xs"
      style={`background-color: ${getCategoryColor(entry.data.category)};`}
    >
      {entry.data.category}
    </span>
  )}
  
  {titleLevel === "h3" ? (
    <h3 class="m-0 mb-2 font-semibold">{entry.data.word}</h3>
  ) : (
    <h4 class="m-0 mb-2 font-semibold">{entry.data.word}</h4>
  )}
  
  <p class="m-0 line-clamp-2 text-sm">{entry.data.meaning}</p>
  
  <span class="mt-2 inline-flex items-center gap-1 text-xs">
    {t("word.learn_more")}
    <ArrowRight size={12} />
  </span>
</a>
PropTypeRequiredDefaultDescription
entryCollectionEntryYes-Word data from content collection
langstringYes-Current language (es/en)
titleLevel”h3” | “h4”No”h4”Heading level for SEO

SearchBox Component

Search input with keyboard shortcut support.
---
interface Props {
  placeholder?: string;
}

const { placeholder = "Search..." } = Astro.props;
---

<form role="search" class="search-form">
  <input
    type="search"
    name="q"
    placeholder={placeholder}
    class="search-input"
    autocomplete="off"
    aria-label="Search words"
  />
</form>

<style>
  .search-input {
    width: 100%;
    padding: 0.75rem 1rem;
    border-radius: 0.5rem;
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    font-size: 1rem;
  }
  
  .search-input:focus {
    outline: 2px solid var(--primary);
    border-color: var(--primary);
  }
</style>

Homepage Components

HeroSection Component

Homepage hero with title, subtitle, and search.
---
import { Info } from "@lucide/astro";
import SearchBox from "../SearchBox.astro";
import { useTranslations } from "../../utils/i18n";

interface Props {
  lang: string;
}

const { lang } = Astro.props;
const t = useTranslations(lang);
---

<section class="mx-auto max-w-[1100px]">
  <div class="card-with-gradient m-6 grid gap-3.5">
    <h1 class="gradient-text mx-4 mt-4 text-[clamp(1.8rem,2.4vw,2.6rem)]">
      {t("home.title")}
    </h1>
    <p class="text-muted mx-4 text-base">
      {t("home.subtitle")}
    </p>
    <div class="mx-4 mb-4">
      <SearchBox placeholder={t("home.search.placeholder")} />
    </div>
    <div class="text-muted mx-4 mb-4 hidden md:block">
      <span class="inline-flex items-center gap-2">
        <Info size={16} />
        {t("home.search.tip")}
        <kbd class="rounded border px-2 py-0.5">/</kbd>
        {t("home.search.tip2")}
      </span>
    </div>
  </div>
</section>

FeaturedWords Component

Grid of featured word cards.
---
import WordCard from "../WordCard.astro";
import { useTranslations } from "../../utils/i18n";

interface Props {
  words: any[];
  lang: string;
}

const { words, lang } = Astro.props;
const t = useTranslations(lang);
---

<section class="mx-auto max-w-[1100px] px-6">
  <h2 class="mb-6 text-2xl font-bold">{t("home.featured.title")}</h2>
  <div class="grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3.5">
    {words.map((entry) => (
      <WordCard entry={entry} lang={lang} titleLevel="h3" />
    ))}
  </div>
</section>

Schema Components

JSON-LD structured data components for SEO.

WordSchema Component

---
interface Props {
  lang: string;
  siteUrl: string;
  word: any;
  slug: string;
}

const { lang, siteUrl, word, slug } = Astro.props;

const schema = {
  "@context": "https://schema.org",
  "@type": "DefinedTerm",
  "name": word.word,
  "description": word.meaning,
  "inDefinedTermSet": `${siteUrl}/${lang}/indice/`,
  "url": `${siteUrl}/${lang}/palabras/${slug}/`,
};
---

<script type="application/ld+json" set:html={JSON.stringify(schema)} />

Creating New Components

Basic Component Template

1

Create the file

Create a new .astro file in src/components/:
touch src/components/MyComponent.astro
2

Define the component

---
// TypeScript props interface
interface Props {
  title: string;
  description?: string;
}

const { title, description } = Astro.props;
---

<div class="my-component">
  <h2>{title}</h2>
  {description && <p>{description}</p>}
</div>

<style>
  .my-component {
    padding: 1rem;
    border-radius: 0.5rem;
  }
</style>
3

Use the component

---
import MyComponent from "../components/MyComponent.astro";
---

<MyComponent title="Hello" description="World" />

Component with Client Script

For interactive components, add a <script> tag:
---
interface Props {
  buttonText: string;
}

const { buttonText } = Astro.props;
---

<button id="my-button" class="btn">{buttonText}</button>

<script>
  document.addEventListener("astro:page-load", () => {
    const button = document.getElementById("my-button");
    button?.addEventListener("click", () => {
      alert("Clicked!");
    });
  });
</script>
Use astro:page-load event for scripts that should work with View Transitions.

Component with Slots

Use slots for flexible content:
---
interface Props {
  title: string;
}

const { title } = Astro.props;
---

<section class="card">
  <h2>{title}</h2>
  <div class="content">
    <slot />  <!-- Default slot -->
  </div>
  <footer>
    <slot name="footer" />  <!-- Named slot -->
  </footer>
</section>
Usage:
<MyCard title="Title">
  <p>This goes in the default slot</p>
  <div slot="footer">Footer content</div>
</MyCard>

Best Practices

interface Props {
  title: string;
  count?: number;  // Optional prop
}

const { title, count = 0 } = Astro.props;  // Default value
Each component should have a single, clear purpose. Break large components into smaller, reusable pieces.
Styles in <style> tags are automatically scoped to the component:
<style>
  .my-class {
    color: red;  /* Only affects this component */
  }
</style>
<style>
  .card {
    background: var(--card);
    color: var(--text);
    border: 1px solid var(--border);
  }
</style>
For components that need translations:
---
import { useTranslations } from "../utils/i18n";

interface Props {
  lang: string;
}

const { lang } = Astro.props;
const t = useTranslations(lang);
---

Component Utilities

Category Colors

import { getCategoryColor } from "../utils/categoryColors";

const color = getCategoryColor("sustantivo");  // Returns hex color

Translations

import { useTranslations } from "../utils/i18n";

const t = useTranslations("es");
const title = t("home.title");

Next Steps

Layouts

Learn about layout components

Styling

Deep dive into styling

Internationalization

Work with translations

Icons

Using Lucide icons

Build docs developers (and LLMs) love