# Component Basics

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Build Pages
Source: https://www.devtutorials.ai/learn/astro/build-pages/component-basics

> The .astro file format, the component script and template, expressions, dynamic attributes, conditional rendering, lists with map(), fragments, dynamic tags, props with defaults and spreading, and importing components.

## Astro Components

- An **Astro component** is a reusable piece of a page, saved in an `.astro` file.
- A header, a blog card and a footer are all components.
- You build a page by putting small components together, like building blocks.
- In this lesson, you will write components and pass data into them.

**Simple lesson:** write a piece of page once as a component, then reuse it everywhere.

### A Real-World Example

- Asha's home page lists her three newest posts.
- Each post shows a title, a date and a short summary in the same box shape.
- Writing the same box three times is slow, and a change must be made three times.
- So she writes one `PostCard` component and uses it three times, with different data.

Press **▶ Run**. Then open `PostCard.astro` and change the box.

<main>
</main>
`,
    "src/components/Header.astro": `<header>
    <strong>Asha's Dev Journal</strong>
</header>
`,
    "src/components/PostCard.astro": `---
const { title, date } = Astro.props;
---

<article>
    <h2>{title}</h2>
    <p>Posted on {date}</p>
</article>
`,
    "src/components/Footer.astro": `---
const year: number = 2026;
---

<footer>© {year} Asha</footer>
`,
  }}
/>

What happened:

- The page imports three components and uses them like HTML tags.
- `PostCard` is used three times. Each time it gets a different `title` and `date`.
- The **HTML sent to browser** view shows three finished `<article>` boxes. The components are gone.

### What You Will Learn

In this lesson, you will learn:

1. The two parts of an `.astro` file.
2. How to show values with `{ }` expressions.
3. How to set attributes from values.
4. How to show something only when a condition is true.
5. How to make a list from an array with `map()`.
6. What fragments and dynamic tags are.
7. How to pass props, read them and give them default values.
8. How to import and organise components.

## Component Anatomy

### The .astro File Format

An `.astro` file has two parts:

- **Component script** = the code between the two `---` lines at the top.
- **Component template** = the HTML-like part below the second `---`.
- The two `---` lines are called the **code fence**. Some people call the script part the **frontmatter**.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 240" role="img" aria-labelledby="fig-anatomy" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-anatomy">An .astro file: the component script between two fence lines runs at build time. The template below becomes HTML.</title>
  <rect x="20" y="16" width="330" height="208" rx="10" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)", strokeWidth: 1.5 }} />
  <rect x="32" y="30" width="306" height="84" rx="6" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} />
  <rect x="32" y="124" width="306" height="88" rx="6" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }} />
  <g style={{ fontFamily: "var(--mono)", fontSize: 13 }}>
    <text x="44" y="50" style={{ fill: "var(--ngc-purple)", fontWeight: 700 }}>---</text>
    <text x="44" y="70" style={{ fill: "var(--ink)" }}>const name = "Asha";</text>
    <text x="44" y="90" style={{ fill: "var(--ink)" }}>const posts = 3;</text>
    <text x="44" y="108" style={{ fill: "var(--ngc-purple)", fontWeight: 700 }}>---</text>
    <text x="44" y="150" style={{ fill: "var(--ink)" }}>&lt;h1&gt;Hi, {"{"}name{"}"}&lt;/h1&gt;</text>
    <text x="44" y="172" style={{ fill: "var(--ink)" }}>&lt;p&gt;{"{"}posts{"}"} posts&lt;/p&gt;</text>
  </g>
  <g style={{ fontFamily: "var(--sans)", fontSize: 13 }}>
    <text x="372" y="56" style={{ fill: "var(--ngc-purple)", fontWeight: 700 }}>Component script</text>
    <text x="372" y="76" style={{ fill: "var(--ink2)" }}>JavaScript or TypeScript</text>
    <text x="372" y="96" style={{ fill: "var(--ink2)" }}>Runs at build time, not in the browser</text>
    <text x="372" y="150" style={{ fill: "var(--ngc-green)", fontWeight: 700 }}>Component template</text>
    <text x="372" y="170" style={{ fill: "var(--ink2)" }}>HTML plus {"{ }"} expressions</text>
    <text x="372" y="190" style={{ fill: "var(--ink2)" }}>Becomes the HTML of the page</text>
  </g>
</svg>
<figcaption>The script prepares the data. The template shows it.</figcaption>
</figure>

### The Component Script

- It is written in JavaScript or TypeScript.
- Use it to make variables, import components and read data.
- It runs when the page is built. It never runs in the visitor's browser.
- So it can safely read files or secret keys.

The script is private, but the template is public. If you write a secret key into the template, it is in the HTML that every visitor downloads.

### The Component Template

- It looks like HTML.
- Inside `{ }`, you can use the variables from the script.
- It can be plain HTML with no script at all. Then the two `---` lines are optional.

**Simple lesson:** the script prepares, the template shows.

## Template Expressions

### Variables and Expressions

- **Expression** = a piece of JavaScript inside `{ }` in the template.
- `{name}` shows the value of `name`.
- You can also do small sums or calls: `{posts * 2}`, `{name.toUpperCase()}`.

### Dynamic Attributes

- An attribute can take a value too: `<a href={link}>`.
- Write `{link}` with no quotes around it.
- With quotes, `"{link}"` is plain text. Astro does not fill it in.

Press **▶ Run**. Then change the values in the script and run again.

<p>I have written {posts} posts. That is {posts * 2} cups of coffee.</p>
<a href={profile} title={name + " on GitHub"}>My GitHub</a>
<button disabled={!isHiring}>Hire me</button>
`,
  }}
/>

- `title={name + " on GitHub"}` builds the text from a variable.
- `disabled={!isHiring}` is `true`, so the button is disabled.
- When an attribute's value is `false`, `null` or `undefined`, Astro leaves the attribute out.

**Simple lesson:** use `{ }` for any value that comes from your script, in text or in attributes.

## Conditions and Lists

### Conditional Rendering

- **Conditional rendering** = show something only when a condition is true.
- `{isNew && <span>New</span>}` shows the span only when `isNew` is true.
- `{online ? <p>Online</p> : <p>Away</p>}` picks one of two things.

### Rendering Lists with map()

- `map()` turns each item of an array into a piece of HTML.
- `{skills.map((skill) => <li>{skill}</li>)}` makes one `<li>` per skill.
- Astro does this at build time. The browser gets finished `<li>` tags.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="fig-map" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-map">Animation: each item of the array HTML, CSS, Astro moves through map and becomes one li tag.</title>
  <defs><marker id="mp-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <text x="110" y="26" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Array</text>
  <text x="320" y="26" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>map()</text>
  <text x="530" y="26" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>HTML</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>
    <rect x="50" y="40" width="120" height="28" rx="6" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="110" y="59" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>"HTML"</text>
    <rect x="50" y="78" width="120" height="28" rx="6" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="110" y="97" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>"CSS"</text>
    <rect x="50" y="116" width="120" height="28" rx="6" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="110" y="135" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>"Astro"</text>
    <rect x="460" y="40" width="140" height="28" rx="6" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.25;0.3;1" dur="6s" repeatCount="indefinite" /></rect>
    <text x="530" y="59" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.25;0.3;1" dur="6s" repeatCount="indefinite" />&lt;li&gt;HTML&lt;/li&gt;</text>
    <rect x="460" y="78" width="140" height="28" rx="6" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.5;0.55;1" dur="6s" repeatCount="indefinite" /></rect>
    <text x="530" y="97" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.5;0.55;1" dur="6s" repeatCount="indefinite" />&lt;li&gt;CSS&lt;/li&gt;</text>
    <rect x="460" y="116" width="140" height="28" rx="6" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.75;0.8;1" dur="6s" repeatCount="indefinite" /></rect>
    <text x="530" y="135" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.75;0.8;1" dur="6s" repeatCount="indefinite" />&lt;li&gt;Astro&lt;/li&gt;</text>
  </g>
  <rect x="260" y="70" width="120" height="44" rx="10" style={{ fill: "var(--ngc-orange)" }} />
  <text x="320" y="97" textAnchor="middle" style={{ fill: "#fff", fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>skill =&gt; &lt;li&gt;</text>
  <path d="M170 92 H256" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#mp-arrow)" />
  <path d="M380 92 H456" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#mp-arrow)" />
  <circle r="6" style={{ fill: "var(--ngc-orange)" }}><animateMotion dur="1.5s" repeatCount="indefinite" path="M170 92 H456" /></circle>
</svg>
<figcaption>map() visits each item and returns one piece of HTML for it.</figcaption>
</figure>

Press **▶ Run**. Then set `isAvailable` to `false` and add a skill to the array.

{isAvailable && <p class="badge">Open to internships</p>}

<ul>
    {skills.map((skill) => (
      <li>
        {skill}
        {skill === newest ? <strong> (new!)</strong> : null}
      </li>
    ))}
</ul>

<p>{skills.length > 3 ? "Growing fast!" : "Just getting started."}</p>
`,
  }}
/>

- `&&` shows the badge only while `isAvailable` is true.
- `? :` picks the last sentence from the number of skills.
- `null` shows nothing.

In Astro, a list made with `map()` needs no `key` attribute. Astro builds HTML once and never updates it, so it does not need keys to track items.

**Simple lesson:** `&&` for "show or hide", `? :` for "this or that", `map()` for lists.

## Fragments and Dynamic Tags

### Fragments

- **Fragment** = a wrapper that groups elements but adds no HTML tag of its own.
- Write it as `<>...</>` or `<Fragment>...</Fragment>`.
- Use it when a condition or `map()` must return more than one element.

### Dynamic Tags

- **Dynamic tag** = a tag whose name comes from a variable.
- Store the tag name in a variable that starts with a capital letter: `const Heading = "h2";`.
- Then write `<Heading>Hi</Heading>`. Astro makes an `<h2>`.

{showContact && (
    <>
      <p>Email: asha@example.com</p>
      <p>City: Chennai</p>
    </>
)}
`,
  }}
/>

- Change `level` to `3`, and the heading becomes an `<h3>`.
- Open **HTML sent to browser**. The two paragraphs have no extra wrapper around them.

**Simple lesson:** a fragment groups without adding a tag. A capitalised variable can be a tag name.

## Props

### Passing Props to Components

- **Props** = values you pass into a component, like settings.
- You pass them as attributes: `<PostCard title="Hello" />`.
- Text goes in quotes. Any other value goes in `{ }`: `<PostCard likes={5} />`.

### Reading Astro.props

- Inside the component, `Astro.props` is an object with every prop.
- Read the ones you need: `const { title, likes } = Astro.props;`.

### Default Values

- A prop that is not passed is `undefined`.
- Give it a default: `const { tag = "General" } = Astro.props;`.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 160" role="img" aria-labelledby="fig-props" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-props">Animation: the page passes title and likes to PostCard. A dot carries the values from the page into Astro.props.</title>
  <defs><marker id="pr-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="20" y="40" width="250" height="80" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="145" y="64" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>index.astro (parent)</text>
  <text x="145" y="96" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12 }}>&lt;PostCard title="Hi" likes={"{"}5{"}"} /&gt;</text>
  <rect x="370" y="40" width="250" height="80" rx="10" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} />
  <text x="495" y="64" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>PostCard.astro (child)</text>
  <text x="495" y="96" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12 }}>Astro.props = {"{"} title, likes {"}"}</text>
  <path d="M270 80 H366" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#pr-arrow)" />
  <circle r="7" cy="80" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="270;366" dur="1.8s" repeatCount="indefinite" /></circle>
  <text x="320" y="146" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>Props flow one way: from the parent into the child.</text>
</svg>
<figcaption>The parent passes props. The child reads them from Astro.props.</figcaption>
</figure>

### Spreading Props

- `{...post}` passes every field of an object as a prop.
- `<PostCard {...post} />` is the same as `<PostCard title={post.title} likes={post.likes} />`.

### [JS] Props Are Plain Objects

- In JavaScript, `Astro.props` is a plain object. You do not declare anything.
- Read the props you need and give defaults where useful.

### [TS] Describing Props with interface Props

- In TypeScript, write an `interface` named `Props` in the script.
- Astro reads it, and your editor warns you when a prop is missing or the wrong type.
- A `?` after a name makes the prop optional: `tag?: string`.

```ts
interface Props {
  title: string;
  likes: number;
  tag?: string;
}

const { title, likes, tag = "General" } = Astro.props;
```

- Common types: `string`, `number`, `boolean`, `string[]` (a list of text), and a choice like `"draft" | "published"`.

Press **▶ Run**. The third card has no `tag`, so it uses the default.

    <h2>{title}</h2>
    <p>♥ {likes} · #{tag}</p>
</article>
`,
    "src/pages/index.astro": `---

const featured = { title: "Spreading props", likes: 42, tag: "Tips" };
---

`,
  }}
/>

- Switch the top bar to **JavaScript**. The `interface Props` block disappears. The component still works.

**Simple lesson:** pass props like attributes. Read them from `Astro.props`, with defaults for optional ones.

## Reusing Components

### Importing Components

- Import a component in the script: `import PostCard from "../components/PostCard.astro";`.
- The name you import is the tag you use. Start it with a capital letter.
- Lower-case tags, such as `<postcard>`, are treated as plain HTML.

### Organising the Components Folder

- Keep small, general pieces in `src/components`: `Button.astro`, `Card.astro`.
- Group bigger sets in sub-folders: `src/components/blog/PostCard.astro`.
- Name files after what they show, in PascalCase: `PostCard`, not `post-card`.

### Composing Components

- **Composing** = building a component from other components.
- A `PostList` can use `PostCard` many times. A page can use `PostList`.
- Each piece stays small and easy to understand.

If you copy the same block of HTML a third time, stop and make it a component.

**Simple lesson:** small components, imported with capital names, put together into bigger ones.

## TL;DR

1. **Component** = a reusable piece of a page in an `.astro` file.
2. **Component script** = code between the `---` lines. Runs at build time.
3. **Template** = HTML plus `{ }` expressions.
4. **Attribute value** = `href={link}`, with no quotes.
5. **Conditions:**
   - `{ok && <p>Yes</p>}` shows or hides.
   - `{ok ? <A /> : <B />}` picks one.
6. **List** = `{items.map((item) => <li>{item}</li>)}`. No `key` needed.
7. **Fragment** = `<>...</>`. Groups without a tag.
8. **Props** = values passed in. Read with `Astro.props`. Defaults with `=`.
9. **[TS]** `interface Props` describes the props.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `href="{link}"` | Quotes are needed | Write `href={link}` | The page links to the text `{link}` |
| Lower-case component name | Any name works | Import and use `PostCard`, with a capital | Astro outputs an unknown `<postcard>` tag |
| Forgetting the import | Components are found by themselves | Import every component in the script | Build error: the name is not defined |
| `{if (ok) ...}` in the template | Any JavaScript works in `{ }` | Use `&&` or `? :` | The build fails |
| Expecting a variable to update on click | The script runs in the browser | The script runs once, at build time | The page never changes |
| No default for an optional prop | Missing props are empty text | Use `= "default"` | The page shows `undefined` or nothing |

</div>

## Check Yourself

## Exercises

## What to Remember

- An `.astro` file has a script (build time) and a template (HTML).
- Use `{ }` for values, `&&` and `? :` for conditions, and `map()` for lists.
- Props go in like attributes and come out of `Astro.props`.
- Give optional props a default.
- Import components with a capital name, then reuse them.

Next: **Slots and Composition**, where components wrap any content you put inside them.
