Authoring Custom Blade Components
WireKit components are plain Blade anonymous components — no special
build step, no class hierarchy to inherit from. You write your own
custom components alongside WireKit's, mix-and-match in your views, and
the WireKit tooling (wirekit:show, wirekit:export-json,
wirekit:doctor) introspects them through the same machinery it uses
for the shipped catalog.
This page documents the @props([...]) syntax shapes WireKit's
toolchain understands. Following the patterns here ensures your custom
components show up correctly in wirekit:show, in AI-tool
autocomplete, and pass the cleanup-hygiene + parser drift audits.
The Minimum Viable Component
{{-- resources/views/components/my-button.blade.php --}}
@props([
'variant' => 'primary',
'size' => 'md',
])
<button {{ $attributes->class([
'rounded px-3 py-1.5',
'bg-[var(--color-wk-accent)]' => $variant === 'primary',
'bg-[var(--color-wk-bg-muted)]' => $variant === 'neutral',
'text-sm' => $size === 'sm',
'text-base' => $size === 'md',
]) }}>
{{ $slot }}
</button>
Use it from any Blade view:
<x-my-button>Save</x-my-button>
<x-my-button variant="neutral" size="sm">Cancel</x-my-button>
WireKit's tooling immediately recognizes the props:
php artisan tinker --execute='
use Pushery\WireKit\Support\PropsParser;
dd(PropsParser::parseBlade(resource_path("views/components/my-button.blade.php")));
'
…returns the two prop entries with their defaults.
@props Shape Reference
The parser handles the following expressions inside an @props([...])
block. If your default expression doesn't fit one of these shapes, the
parser may still handle it — but these are the tested-and-guaranteed
cases.
1 · Bare literals
@props([
'count' => 0,
'enabled' => true,
'tag' => 'div',
'maxRows' => null,
])
Primitive types — integers, booleans, strings, null. Always works.
2 · config(...) defaults with commas in arguments
@props([
'variant' => config('myapp.components.button.variant', 'primary'),
])
The parser recognizes commas inside the config(...) argument list as
PART OF the default expression, not as prop boundaries. This was a
historical bug class in the old regex parser; the current tokenizer-
based parser handles it correctly.
3 · Trailing inline // comment
@props([
'trend' => null, // 'up' | 'down' | 'neutral' | null
])
The comment is captured as a separate comment field on the prop
record (NOT as part of the default value). wirekit:show <name>
prints the comment in gray alongside the prop name, making the
allowed-value enumeration discoverable without opening the source file.
4 · match(...) expressions
@props([
'classes' => match (1) {
1 => 'text-4xl',
2 => 'text-3xl',
default => 'text-base',
},
])
Useful for deriving the default from another prop at render time.
5 · Array-literal defaults
@props([
'items' => [1, 2, 3],
'config' => ['enabled' => true, 'limit' => 10],
])
Both flat and nested arrays parse cleanly. Use heredoc syntax (below) for very long inline arrays — readability beats brevity.
6 · Mixed single- and double-quoted keys
@props([
'foo' => 'one',
"bar" => "two",
])
Both quote styles are valid PHP. The parser strips them uniformly when extracting the prop name.
7 · Multi-line default expressions
@props([
'classes' => implode(' ', [
'flex',
'items-center',
'gap-2',
]),
])
The parser tracks bracket / paren nesting via PHP's tokenizer, so multi-line expressions don't trip it up. Indent for readability.
8 · Heredoc / nowdoc defaults
@props([
'template' => <<<'TEMPLATE'
<div>
Hello, world.
</div>
TEMPLATE,
])
Useful for long inline strings that contain special characters
(backticks, double quotes, </script> sequences).
What the Parser Returns
use Pushery\WireKit\Support\PropsParser;
$props = PropsParser::parseBlade('resources/views/components/my-button.blade.php');
Each entry in the returned list has the same shape:
[
'name' => 'variant',
'default' => "'primary'", // Raw source text.
'default_normalized' => "'primary'", // Whitespace-collapsed.
'type_hint' => null, // Reserved for future @phpdoc augmentation.
'comment' => null, // Or "'up' | 'down'" etc.
]
The same data drives wirekit:show <name>, wirekit:show <name> --as=json,
and wirekit:export-json.
CLI Tooling for Custom Components
Once your component is in resources/views/components/, every WireKit
CLI command sees it:
# Programmatic introspection — same JSON shape as shipped components
php artisan wirekit:show my-button --as=json
# Lint a developer Blade file against your component's prop set
php artisan wirekit:show my-button --validate-against=resources/views/pages/checkout.blade.php
Note: wirekit:list and the registry-based commands only list
components that are in Pushery\WireKit\ComponentRegistry. Custom
components in resources/views/components/ are visible to
wirekit:show (which file-walks) but not to wirekit:list (which
reads the registry). If you want your custom component to show up in
the registry, register it via your service provider — but the typical
pattern is "custom components stay local, only WireKit's catalog
lives in the registry".
Common Pitfalls
Comma INSIDE a function-call argument list — historical bug class
// PARSES CORRECTLY in v2.0.0+. The old regex parser broke here.
'variant' => config('myapp.x.y', 'fallback'),
If you see wirekit:show <name> printing a truncated config(...)
expression OR a phantom "next prop" you didn't write, you're either on
an old WireKit version OR there's a parser regression — file a bug
with the prop source.
Trailing inline comment leaking into the default — historical bug class
// PARSES CORRECTLY. The comment is captured as a separate field.
'trend' => null, // 'up' | 'down' | 'neutral'
Same as above — historical bug, fixed in v2.0.0.
Variable / expression as the array key
// Don't do this. PHP allows it but `@props` doesn't.
@props([
$dynamicKey => 'value', // ← parser can't extract a string name
])
@props array keys MUST be string literals. The parser silently skips
non-string-literal keys.
Asserting strict-mode warnings in tests
When app.debug is true (or you run in the console), WireKit emits a
warning-level log for an unknown prop key passed to a component — a
Did-you-mean signal that a typo'd prop (<x-wirekit::button variantt="…">) was
silently dropped into the attribute bag. It is logged through the strictness
gate and never throws.
To assert that warning in a test you need it to (a) NOT land in
storage/logs/laravel*.log — otherwise the wirekit:verify silent-typo
log-scan would later report it as a finding — and (b) stay type-clean under
static analysis (Larastan). Two common reaches don't fit:
Log::fake()— Laravel has no such method (unlikeEvent::fake()/Queue::fake()); calling it errors withundefined method …::fake().Log::shouldReceive(…)— a Mockery facade expectation, which Larastan flags at higher levels on the dynamically-typed return.
Swap the logger for a tiny array recorder instead — it replaces the channel entirely (nothing reaches the file handler) and uses only typed PSR-3 methods, so it is both log-scan-safe and Larastan-clean:
use Illuminate\Support\Facades\Log;
use Pushery\WireKit\WireKit;
use Psr\Log\AbstractLogger;
it('warns on an unknown prop key in debug mode', function () {
// 1. Strict-mode warnings only emit when app.debug is on.
config()->set('app.debug', true);
// 2. Replace the logger with an array recorder — no disk write, no Mockery.
$records = [];
Log::swap(new class($records) extends AbstractLogger {
public function __construct(private array &$records) {}
public function log($level, string|\Stringable $message, array $context = []): void
{
$this->records[] = [(string) $level, (string) $message];
}
});
// 3. Drive the unknown-prop path. WireKit's components call this helper
// internally; in a focused unit test you can call it directly with a key
// that isn't a declared prop (stable across releases, unlike a specific
// component's prop list).
WireKit::warnUnknownProps('button', ['variantt' => 'ghost'], ['variant', 'surface']);
// 4. Assert the warning fired — captured in memory, never written to disk.
expect($records)->toHaveCount(1)
->and($records[0][0])->toBe('warning')
->and($records[0][1])->toContain('Unknown prop "variantt"');
});
See Also
- ComponentRegistry — Programmatic Discovery — reading the catalog at runtime.
- Authoring Custom Alpine Plugins — defensive-cleanup pattern for plugins that drive your components.
- CLI Reference — full command surface.