Skip to content

Start building your next app or website — with AI, right in your browser.

Visit devdojo.com
Docs
Components

Sites 06 / 13

Components

Build a nav or a card once, use it anywhere, and edit it in one place forever.

Components

Creating a reusable piece of your site is as simple as creating a .blade.php file inside the resources/views/components/ directory. The nav, the footer, that call-to-action panel you're proud of — build it once, use it anywhere, and every copy stays in sync.

Create once, use everywhere

A component is just a Blade file in your components/ folder:

<!-- resources/views/components/nav.blade.php -->
<nav class="flex items-center justify-between px-6 py-4">
    <a href="/" class="font-bold">My Site</a>
    <a href="/about" class="text-zinc-500">About</a>
</nav>

Drop it into any page or layout with an <x-…> tag — the name after x- is the file (without .blade.php):

<x-nav/>

Folders become dots: components/sections/hero.blade.php is used as <x-sections.hero/>. Both the self-closing form (<x-nav/>) and the paired form (<x-nav>…</x-nav>) work — the paired form is how you pass a slot (more below).

Edit once, update everywhere ✨

This is the whole point of components. Your nav is used on ten pages? Change components/nav.blade.php once and all ten pages update instantly. No hunting, no copy-paste drift.

In Code mode, the components/ folder in the file tree is home base for your reusable pieces — create, open, and edit them there.

Props: same component, different content

Declare the attributes a component accepts with @props at the top of the file, then echo them as variables. Pass them on the tag:

<!-- resources/views/components/card.blade.php -->
@props(['title' => 'Untitled', 'body' => ''])
<div class="rounded-xl border p-6">
    <h3 class="font-semibold">{{ $title }}</h3>
    <p class="mt-2 text-sm text-zinc-500">{{ $body }}</p>
</div>
<!-- any page -->
<x-card title="Fast" body="No build step. Ever." />
<x-card title="Simple" body="Just HTML and Tailwind." />

One component, two cards, one place to change the design. A few details worth knowing:

  • @props(['title' => 'Untitled']) declares each prop with a default. The default is used when the tag omits that attribute — so a bare <x-card/> still renders. Defaults live right here, in @props.
  • Attribute values go in double quotestitle="Fast". Need a literal quote inside? Write it as &quot;.
  • Every echoed variable must be declared in @props, or be the global $site, or be a global collection. A {{ $subtitle }} you never declared is a build error — your cue to add it to @props. Give optional props an empty default: @props(['subtitle' => '']).

Components and data: collections, $site, and bound attributes

Components see three things automatically: their own declared props, the global $site, and every collection under resources/data/collections/ — by name, with zero wiring. That's what makes a nav component backed by resources/data/collections/links.json just work, no matter how deeply it's nested inside a layout: declare no prop for links at all (or a bare @props(['links' => null])) and read $links directly.

<!-- resources/views/components/nav.blade.php — no prop, no bound attribute -->
<nav class="flex items-center justify-between px-6 py-4">
    @foreach ($links as $link)
        <a href="{{ $link->url }}">{{ $link->text }}</a>
    @endforeach
</nav>

A default can't win against a collection. A @props default can never override an already-present collection of the same name — only a value the tag actually passes wins. So a prop named after a collection is effectively reserved for it; give a component's own, purely-local prop a different name.

Reach for a bound attribute — an attribute with a : prefix, whose value is an expression instead of a literal string — only when you want per-instance data: the same component fed a different slice in different places, under a name that isn't the shared collection's own:

<!-- the page passes its own slice, the component receives it as `items` -->
<x-sections.feature-grid heading="Why teams pick us" :items="$features" />
<!-- resources/views/components/sections/feature-grid.blade.php -->
@props(['heading' => 'Features', 'items'])
<section class="mx-auto max-w-5xl py-20">
    <h2 class="text-3xl font-semibold">{{ $heading }}</h2>
    <ul class="mt-8 grid gap-6 sm:grid-cols-2">
        @foreach ($items as $item)
        <li class="rounded-xl border p-6">
            <h3 class="font-medium">{{ $item->title }}</h3>
            <p class="mt-2 text-zinc-600">{{ $item->description }}</p>
        </li>
        @endforeach
    </ul>
</section>
  • heading="Why teams pick us" passes the literal string Why teams pick us.
  • :items="$features" passes the value of $features (the collection) — the : means "evaluate this as an expression."

Declaring a prop you'll pass in. A prop that receives a collection is declared bare@props(['items']), no default. You can't give an array a default in @props; if you want it optional, default it to null and guard with @if ($items).

Making sections editable: the companion .yml

Components become editable in the builder's section options panel automatically — but you control how:

  • No .yml — the inspector reads your @props and shows a plain text field for each prop that has a string default. Great for headings and short copy, zero extra files.
  • With a companion .yml (same name, .yml extension) — you get typed fields: textareas, URL pickers, image uploaders, color swatches, and dropdowns, each with a friendly label.
# resources/views/components/sections/hero.yml
title: Split hero
description: Headline, supporting copy, and the primary action
fields:
    heading:
        type: text
        label: Heading
    body:
        type: textarea
        label: Supporting copy
    button_text: { type: text, label: Button text }
    button_link: { type: url, label: Button link }
    image: { type: image, label: Image }

Field types: text, textarea (add rows:), url, image, select (add options: { value: Label }), color, number and range (both take min / max / step), and toggle (the value is "1" when on and empty when off — gate markup with @if ($prop)). Inside a repeater's sub_fields: there's one more: richtext — a full rich text editor (headings, bold, lists, links) over an HTML string, made for entry bodies like a blog post's content. And you rarely need to declare it per section: a collection-backed repeater inherits column types from the collection's own schema yml for anything its sub_fields: doesn't cover. Add required: true to mark a field, and description: for a hint under it. The yml declares which props are editable and how; the actual defaults live in the component's @props.

Anything list-like — menu items, FAQ rows, feature cards — is a repeater: declare sub_fields: (scalar types plus richtext), optionally nestable: true for one level of children (dropdown menus), plus add_button_label: and item_label: (the sub-field that titles each row). Repeater data never lands on the tag as a literal value — it's wired one of three ways, and the build checks that it's wired at all (an unwired repeater edits JSON nothing renders):

  1. Collection-backed — the default. Name the field after its collection: field linksresources/data/collections/links.json. The component reads the collection directly ($links) — no prop, no bound attribute, collections are global exactly like $site. Instances stay bare (<x-nav/>). Create the collection file with real content; a same-named @props default never overrides it.
  2. Per-instance — a bound attribute. The same component fed different data in different places: :items="$faqs" edits resources/data/collections/faqs.json (see collections), matched by an @props default for when nothing's bound.
  3. Site-wide — site.json-backed. For values that belong with the rest of the business data: :links="$site->nav_links" edits the nav_links key of resources/data/site.json, and the component reads $site->nav_links directly.

Every section bound to the same data updates together.

Who wins? Editing a field in the options panel writes the value back onto that page's <x-…> tag as an attribute — it's that page's own copy. The component's @props defaults (and every other page using the section) stay untouched.

Editing a component on a page

Once a component is sitting on a page, changing its content on that page works in either face of the builder:

Click the section on the canvas. The inspector opens with a form — one field per editable prop (text fields from @props, or the typed fields from a companion .yml). Edit a field and the new value is written back onto that page's <x-…> tag.

Edit the attributes on the <x-…> tag itself:

<x-card title="Blazing fast" body="Still no build step." />

Change an attribute, and autosave takes care of the rest.

Components inside components

Components can use other components — a footer.blade.php that includes a <x-newsletter-form/>, for example. Nest away. If a component ever references itself in a loop, there's a generous recursion cap so a runaway include can't hang your page.

What if the file doesn't exist?

An <x-…> tag pointing at a missing component doesn't break your page — it renders an HTML comment like <!-- pocketknife:missing component "sections.hero" --> where the component would have been. If a component seems to be silently missing, check the page source in the preview for one of these.

Next up

Components handle repeated markup. For repeated content — team members, features, sites — you want Collections.

© 2026 DevDojo Edit this page