AstroShowcase
Docs / Core Concepts / Components & Props
Core Concepts Updated May 7, 2026

Components & Props

Everything about .astro components — frontmatter, templates, props, slots, and scoped styles.

Anatomy of a component

Every Astro component is a .astro file split into two parts:

---
// ── Frontmatter (server-side script) ──────────────────────
// Runs on the server. Can import, fetch, compute — anything.
// Result variables are available in the template below.

const message = "Hello from the server!"
---

<!-- ── Template (HTML) ──────────────────────────────────── -->
<!-- Uses JSX-like {} syntax to inject server values -->
<p>{message}</p>

Key rule: code in --- fences never runs in the browser. It’s removed entirely before the page is sent to the user.

Defining and using props

Props let parent components pass data into a child:

---
// Button.astro — child component
interface Props {
  label:    string
  href?:    string
  variant?: 'primary' | 'outline'
}

const { label, href = '#', variant = 'primary' } = Astro.props
---

<a href={href} class={`btn btn-${variant}`}>{label}</a>

Use it in a parent:

---
import Button from './Button.astro'
---

<Button label="Get Started" href="/signup" variant="primary" />
<Button label="Learn More"  href="/docs"   variant="outline" />

Slots

Slots let you pass HTML content into a component — like React’s children:

---
// Card.astro
---
<div class="card">
  <slot />   <!-- default slot — renders whatever the parent passes -->
</div>

Use it:

<Card>
  <h2>My Title</h2>
  <p>Some content inside the card.</p>
</Card>

Named slots

For multiple injection points, name them:

---
// Modal.astro
---
<div class="modal">
  <header><slot name="title" /></header>
  <main><slot /></main>
  <footer><slot name="actions" /></footer>
</div>
<Modal>
  <span slot="title">Confirm Delete</span>
  <p>Are you sure? This cannot be undone.</p>
  <div slot="actions">
    <button>Cancel</button>
    <button class="danger">Delete</button>
  </div>
</Modal>

Scoped styles

Add a <style> block in your component and Astro automatically scopes it — styles only apply to that component, never leak out:

<h1 class="title">Hello</h1>

<style>
  /* This ONLY applies to .title in THIS file */
  .title {
    color: coral;
    font-size: 2rem;
  }
</style>

For global styles use <style is:global>:

<style is:global>
  /* Applies everywhere */
  body { margin: 0; }
</style>

Client-side scripts

Add a <script> tag for browser JavaScript:

<button id="counter">Count: 0</button>

<script>
  // This DOES run in the browser
  const btn = document.getElementById('counter')!
  let count = 0

  btn.addEventListener('click', () => {
    count++
    btn.textContent = `Count: ${count}`
  })
</script>

Astro bundles scripts automatically. Multiple components with <script> blocks get their scripts deduplicated and bundled together.

Expressions in templates

Inside the HTML template you can use any JavaScript expression inside {}:

---
const isLoggedIn = true
const items      = ['Apple', 'Banana', 'Cherry']
const color      = 'orange'
---

<!-- Conditional rendering -->
{isLoggedIn ? <p>Welcome back!</p> : <a href="/login">Sign in</a>}

<!-- Short-circuit (render if true) -->
{isLoggedIn && <p>You are logged in.</p>}

<!-- List rendering -->
<ul>
  {items.map(item => <li>{item}</li>)}
</ul>

<!-- Dynamic class -->
<div class={`text-${color}`}>Colored text</div>

<!-- class:list utility for conditional classes -->
<button
  class:list={[
    'btn',
    { 'btn-active': isLoggedIn },
    color === 'orange' && 'btn-orange',
  ]}
>
  Click me
</button>

Fragments

Astro components can return multiple top-level elements — no wrapping <div> needed:

---
// MultiReturn.astro — works fine, no wrapper required
---
<h1>Title</h1>
<p>Paragraph one</p>
<p>Paragraph two</p>

If you need an explicit fragment for template logic, use <Fragment> or <>:

{condition && (
  <>
    <p>First paragraph</p>
    <p>Second paragraph</p>
  </>
)}