Sites 08 / 15
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 quotes —
title="Fast". Need a literal quote inside? Write it as". - 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 stringWhy 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@propsand 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,.ymlextension) — 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):
One exception, on purpose: when a repeater's collection is a content type — its entries have their own pages, like a blog's posts — the panel shows a collection card instead of editable rows. The card names the collection, counts what's published and drafted, and offers Manage (the entries list) and New (the entry writer). Posts are real content with their own bodies and URLs, so they're managed in the writer — where deleting one is a deliberate, confirmed act — rather than as rows whose ✕ would erase the post outright.
- Collection-backed — the default. Name the field after its collection: field
links↔resources/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@propsdefault never overrides it. - Per-instance — a bound attribute. The same component fed different data in different places:
:items="$faqs"editsresources/data/collections/faqs.json(see collections), matched by an@propsdefault for when nothing's bound. - Site-wide —
site.json-backed. For values that belong with the rest of the business data::links="$site->nav_links"edits thenav_linkskey ofresources/data/site.json, and the component reads$site->nav_linksdirectly.
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.