Lesson 6 — Blade, Components and Vite: give TaskFlow a reusable layout, an empty-state component and built CSS. Continue from lesson 5 without changing the controller, middleware or data. There are no persisted tasks yet, so we will not invent dashboard statistics.
1. Divide the interface by responsibility
The layout owns the HTML document, title, navigation and assets. The page selects its content. The empty-state component represents a reusable UI element. Split templates where responsibilities are clear rather than turning every HTML tag into its own component.
Blade renders HTML on the server; Vite processes frontend assets. A Blade component does not require JavaScript to appear. The Blade documentation describes props, attributes and slots. Here we use anonymous components without separate PHP classes.
2. Create the layout
Create resources/views/components/layout.blade.php:
@props(['title' => 'TaskFlow'])
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<a href="#main-content">Skip to content</a>
<nav aria-label="Main"><a href="{{ route('taskflow.overview') }}">TaskFlow overview</a></nav>
<main id="main-content" class="taskflow-shell">{{ $slot }}</main>
</body>
</html>The title has a default, while the slot contains template HTML supplied by the caller. The skip link targets main-content, and route() uses the route name rather than a hardcoded path. A component slot contains rendered template content; it is not an arbitrary user-HTML sanitizer.
3. Create and compose the empty state
Create resources/views/components/empty-state.blade.php:
@props(['title'])
<section {{ $attributes->class(['taskflow-empty']) }}>
<h2>{{ $title }}</h2>
<p>{{ $slot }}</p>
</section>Replace resources/views/taskflow/overview.blade.php with:
<x-layout :title="$projectName">
<h1>{{ $projectName }}</h1>
<p>Milestone: {{ $milestone }}</p>
<x-empty-state title="No tasks yet" data-testid="empty-state">
No tasks have been loaded yet.
</x-empty-state>
</x-layout>The colon in :title="$projectName" passes a PHP expression; title="No tasks yet" passes a literal string. @props extracts data properties; remaining attributes such as data-testid stay in the attribute bag. class() combines the default class with additional classes. Keep attribute names developer-controlled rather than turning user input into event handlers.
Use double braces for untrusted text. Do not switch to raw Blade output to allow arbitrary user HTML. HTML escaping does not replace URL validation, authorization or appropriate JavaScript encoding: each context has its own requirements.
4. Add CSS and build assets
Add this CSS to resources/css/app.css while preserving the skeleton's existing declarations:
body { font-family: system-ui, sans-serif; line-height: 1.6; color: #172033; background: #f4f7fb; padding: 1rem; }
a { color: #1549ad; text-decoration: underline; }
a:focus-visible { outline: 3px solid #1549ad; outline-offset: 3px; }
.taskflow-shell { max-width: 60rem; margin: 2rem auto; }
.taskflow-shell h1 { font-size: 2rem; font-weight: 700; }
.taskflow-empty { margin-top: 1rem; padding: 1.5rem; border: 1px solid #9aa8ba; border-radius: .75rem; background: white; }
.taskflow-empty h2 { font-size: 1.25rem; font-weight: 600; }TaskFlow's vite.config.js already declares resources/css/app.css and resources/js/app.js as inputs. The layout must reference matching entry points. Do not hardcode hashed CSS filenames that change during builds. Laravel's Vite integration selects the appropriate assets through @vite.
npm run build
php artisan test
php artisan serve --host=127.0.0.1 --port=8000
A build creates public/build/manifest.json. For continuous editing, run npm run dev in a separate terminal. Understand whether you are checking development assets or a production build instead of unnecessarily running both modes. Do not expose a Vite development server as your production asset server.
5. Test the output and understand the limits
Create tests/Feature/TaskFlowBladeTest.php:
<?php
namespace Tests\Feature;
use Tests\TestCase;
class TaskFlowBladeTest extends TestCase
{
public function test_layout_escapes_the_name_and_renders_component_attributes(): void
{
$this->withoutVite();
config(['taskflow.name' => '<script>alert(1)</script>']);
$this->get('/taskflow')->assertOk()
->assertSee('<script>alert(1)</script>', escape: false)
->assertDontSee('<script>alert(1)</script>', escape: false)
->assertSee('data-testid="empty-state"', escape: false)
->assertSeeText('No tasks yet')
->assertSee('id="main-content"', escape: false);
}
}withoutVite() affects this test only, isolating HTML rendering from the manifest. Other HTTP tests still use Vite, so build before running the complete suite. The script assertions compare raw response HTML; escape: false in an assertion does not disable Blade escaping.
Verified results: Vite built successfully and the complete suite passed 9 tests with 24 assertions. The optional fontaine warning from the skeleton remains non-fatal. This test does not execute JavaScript, measure contrast or prove assets return HTTP 200 through a web server. Separately inspect browser Network requests, keyboard focus, narrow layouts and console errors.
6. Practice checklist
- Add a class to x-empty-state and verify that its default class remains.
- Change the configured display name; title and h1 should both update while markup-like input remains escaped text.
- For component-not-found errors, compare x-empty-state with components/empty-state.blade.php.
- For a missing manifest, build in the correct project; for a stopped development server, inspect Vite and its hot-file state.
- For stale layout output, confirm the server and use view:clear when appropriate, rather than deleting vendor.
Navigation: Lesson 5 · Roadmap. Next we add forms, validation and CSRF before designing the database.




No comments yet. Be the first to share your thoughts.