zairakai/laravel-blade-components

Collection of reusable Blade components for forms, layouts, navigation and UI elements with Laravel integration

Fund package maintenance!
Patreon
Other

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Forks: 0

pkg:composer/zairakai/laravel-blade-components

v1.1.1 2026-02-23 16:23 UTC

This package is auto-updated.

Last update: 2026-02-23 15:27:07 UTC


README

Main Develop

Packagist Downloads

License PHP Laravel

Static Analysis Code Style

62 reusable Blade components for Laravel — forms, layout, content and media — with built-in i18n (21 locales), accessibility attributes, and zero CSS dependencies.

Table of Contents

Requirements

DependencyVersion
PHP^8.3
Laravel^10.0 \| ^11.0

Installation

composer require zairakai/laravel-blade-components

The service provider is auto-discovered. No additional setup required.

Configuration

The package ships with sensible defaults. Publish the config to override them:

php artisan vendor:publish --tag=zairakai-config

This creates config/blade-components.php:

return [
    'password' => [
        'min_characters' => 8,          // Minimum length for <x-zk-password>
    ],
    'email' => [
        'pattern' => '[^@]+@[^@]+\.[a-zA-Z]{2,}',  // Regex for <x-zk-email>
    ],
    'select' => [
        'icon_after' => 'keyboard_arrow_down',       // Trailing icon for <x-zk-select>
    ],
];

Publishing assets

TagContentsDestination
zairakai-componentsBlade views per categoryresources/views/vendor/zairakai/
zairakai-langTranslation files (21 locales)lang/vendor/zairakai/
zairakai-configConfiguration fileconfig/blade-components.php
zairakai-allAll of the above
# Publish everything at once
php artisan vendor:publish --tag=zairakai-all

# Or selectively
php artisan vendor:publish --tag=zairakai-components
php artisan vendor:publish --tag=zairakai-lang
php artisan vendor:publish --tag=zairakai-config

Components

All public components use the zk- prefix. Internal cross-component references (used inside Blade templates) also work without prefix — they are registered automatically.

Content components

ComponentTagDescription
Blockquote<x-zk-blockquote>Styled <blockquote> with optional cite
Heading<x-zk-heading>h1h6 with level prop
Link<x-zk-link>Anchor with route resolution, icons, active state
List<x-zk-list>Recursive ul/ol supporting links, images, text
MSR<x-zk-msr>Material Symbols / Rounded icon wrapper
Paragraph<x-zk-paragraph><p> with id and class support

Link

{{-- Simple href --}}
<x-zk-link href="https://example.com" target="_blank" rel="noopener">
    Visit site
</x-zk-link>

{{-- Named route — adds class="active" when current --}}
<x-zk-link :route="'dashboard'" :routeParams="['id' => 1]">
    Dashboard
</x-zk-link>

{{-- With Material Symbols icon --}}
<x-zk-link route="profile" msr="person">
    My Profile
</x-zk-link>

List

<x-zk-list :items="[
    ['type' => 'href', 'href' => '/home', 'label' => 'Home'],
    ['type' => 'route', 'route' => 'dashboard', 'label' => 'Dashboard'],
    ['type' => 'text', 'label' => 'Plain text item'],
    [
        'type' => 'href', 'href' => '/parent', 'label' => 'Parent',
        'children' => [
            'ordered' => false,
            'children' => [
                ['type' => 'href', 'href' => '/child', 'label' => 'Child'],
            ],
        ],
    ],
]" />

Heading

<x-zk-heading :level="1">Page Title</x-zk-heading>
<x-zk-heading :level="2" class="section-title">Section</x-zk-heading>

Form components

ComponentTagDescription
Input<x-zk-input>Universal input (all HTML types) with label, icons, supporting text
Textarea<x-zk-textarea>Textarea with label, character counter
Select<x-zk-select>Select dropdown with optgroups, default option, icons
Checkbox<x-zk-checkbox>Checkbox with optional label wrapper
Radio<x-zk-radio>Radio button
Switch<x-zk-switch>Toggle switch (wraps checkbox)
Button<x-zk-button><button> with field wrapper, icons
Submit<x-zk-submit>Submit button (wraps button)
Reset<x-zk-reset>Reset button (wraps button)
Form<x-zk-form><form> with auto CSRF, spoofing, enctype detection
Label<x-zk-label><label> with icon support
Field<x-zk-field>Field wrapper div with error class
Fieldset<x-zk-fieldset><fieldset> with legend before/after slot
Additional<x-zk-additional>Supporting text + character counter
Email<x-zk-email>Email input with configurable pattern
Password<x-zk-password>Password with min-length pattern
Date<x-zk-date>Date input
Datetime<x-zk-datetime>Datetime-local input
Time<x-zk-time>Time input
Week<x-zk-week>Week input
Month<x-zk-month>Month input
Number<x-zk-number>Number input
Range<x-zk-range>Range slider
Color<x-zk-color>Color picker
File<x-zk-file>File upload input
Search<x-zk-search>Search input
Tel<x-zk-tel>Telephone input
URL<x-zk-url>URL input
Hidden<x-zk-hidden>Hidden input
Datalist<x-zk-datalist><datalist> element

Input

{{-- Basic --}}
<x-zk-input name="username" label="Username" required />

{{-- With icons, prefix and supporting text --}}
<x-zk-input
    name="amount"
    label="Amount"
    type="number"
    prefix="€"
    iconAfter="euro"
    supportingText="Minimum order: €10"
    :min="10"
/>

{{-- Without field wrapper (bare input) --}}
<x-zk-input name="search" type="search" :field="false" placeholder="Search…" />

Select

<x-zk-select
    name="country"
    label="Country"
    :options="$countries"          {{-- Collection or array --}}
    :selected="$user->country"
    required
    labelBefore
/>

{{-- With optgroups --}}
<x-zk-select
    name="category"
    :options="[
        'Fruits' => ['apple' => 'Apple', 'banana' => 'Banana'],
        'Vegs'   => ['carrot' => 'Carrot'],
    ]"
/>

Form

{{-- POST with CSRF auto-injected --}}
<x-zk-form route="profile.update" method="PUT">
    <x-zk-input name="name" label="Full name" :value="$user->name" required />
    <x-zk-submit>Save</x-zk-submit>
</x-zk-form>

{{-- File upload — enctype detected automatically --}}
<x-zk-form route="avatar.store">
    <x-zk-file name="avatar" label="Profile picture" accept="image/*" />
    <x-zk-submit>Upload</x-zk-submit>
</x-zk-form>

Password

{{-- Uses config('blade-components.password.min_characters', 8) --}}
<x-zk-password name="password" label="Password" required />

{{-- Override minimum --}}
<x-zk-password name="password" label="Password" :min="12" required />

Checkbox & Switch

<x-zk-checkbox name="terms" label="I accept the terms" required />

<x-zk-switch name="notifications" label="Email notifications" :checked="$user->notifications" />

Fieldset

<x-zk-fieldset legend="Shipping address">
    <x-zk-input name="address" label="Street" required />
    <x-zk-input name="city" label="City" required />
</x-zk-fieldset>

Layout components

ComponentTagDescription
Header<x-zk-header><header> semantic element
Footer<x-zk-footer><footer> semantic element
Main<x-zk-main><main> semantic element
Nav<x-zk-nav><nav> with optional role
Section<x-zk-section><section> auto-wrapped in container
Article<x-zk-article><article> semantic element
Aside<x-zk-aside><aside> semantic element
Container<x-zk-container><div class="container">
Wrapper<x-zk-wrapper><div class="wrapper">
Grid<x-zk-grid>CSS grid wrapper with columns prop
Grid Item<x-zk-grid-item>Grid cell
Row<x-zk-row>Flex row
Column<x-zk-column>Flex/grid column with col prop
Breadcrumb<x-zk-breadcrumb><nav> breadcrumb with structured items
Tabs<x-zk-tabs>Tabbed interface
Pagination<x-zk-pagination>Page navigation

Nav

<x-zk-nav role="navigation" class="main-nav">
    <x-zk-link route="home">Home</x-zk-link>
    <x-zk-link route="about">About</x-zk-link>
</x-zk-nav>

Breadcrumb

<x-zk-breadcrumb :items="[
    ['label' => 'Home',     'href' => '/'],
    ['label' => 'Products', 'href' => '/products'],
    ['label' => 'Laptop',   'aria-current' => 'page'],
]" />

Grid

<x-zk-grid :columns="3" class="product-grid">
    @foreach ($products as $product)
        <x-zk-grid-item>{{ $product->name }}</x-zk-grid-item>
    @endforeach
</x-zk-grid>

Pagination

<x-zk-pagination :currentPage="$page" :totalPages="$total" />

Tabs

<x-zk-tabs :tabs="[
    ['title' => 'Overview',  'content' => 'Tab 1 content'],
    ['title' => 'Details',   'content' => 'Tab 2 content'],
    ['title' => 'Reviews',   'content' => 'Tab 3 content'],
]" />

Media components

ComponentTagDescription
Image<x-zk-image><img> with srcset, loading, decoding, fetchpriority
Video<x-zk-video><video> with sources and tracks
Audio<x-zk-audio><audio> with sources and tracks
Figure<x-zk-figure><figure> wrapper
Figcaption<x-zk-figcaption><figcaption>
Canvas<x-zk-canvas><canvas> with width/height
Iframe<x-zk-iframe><iframe> with sandbox, allow
Object<x-zk-object><object> element
Source<x-zk-source><source> element
Track<x-zk-track><track> subtitle/caption element

Image

<x-zk-image
    src="/img/hero.webp"
    alt="Hero image"
    width="1200"
    height="600"
    loading="lazy"
    decoding="async"
    fetchpriority="high"
    srcset="/img/hero-sm.webp 640w, /img/hero.webp 1200w"
    sizes="(max-width: 640px) 100vw, 1200px"
/>

Video

<x-zk-video
    :sources="[
        ['src' => '/video/demo.webm', 'type' => 'video/webm'],
        ['src' => '/video/demo.mp4',  'type' => 'video/mp4'],
    ]"
    :tracks="[
        ['src' => '/subs/en.vtt', 'kind' => 'subtitles', 'srclang' => 'en', 'label' => 'English'],
    ]"
    controls
    poster="/img/poster.webp"
/>

{{-- Single source shorthand --}}
<x-zk-video sources="/video/demo.mp4" controls />

Audio

<x-zk-audio
    :sources="[
        ['src' => '/audio/track.ogg', 'type' => 'audio/ogg'],
        ['src' => '/audio/track.mp3', 'type' => 'audio/mpeg'],
    ]"
    controls
/>

Figure with caption

<x-zk-figure>
    <x-zk-image src="/img/chart.svg" alt="Sales chart" />
    <x-zk-figcaption>Q4 2024 sales by region</x-zk-figcaption>
</x-zk-figure>

Helpers

Three static utility methods are available via BladeHelpers, used internally by the components and accessible in your own Blade views:

MethodSignatureDescription
getOldValueBladeHelpers::getOldValue(?string $name, mixed $value): mixedReturns old($name) if available, fallback to $value
routeIsBladeHelpers::routeIs(string $route): boolChecks if the current route matches the given name
isValidMimeTypeBladeHelpers::isValidMimeType(string $type): boolValidates a MIME type string format
@php
    use Zairakai\LaravelBladeComponents\BladeHelpers;

    $value = BladeHelpers::getOldValue('email', $user->email);
@endphp

@if(BladeHelpers::routeIs('dashboard'))
    {{-- current route is "dashboard" --}}
@endif

{{-- Validate MIME type --}}
{{-- BladeHelpers::isValidMimeType('image/png')  → true  --}}
{{-- BladeHelpers::isValidMimeType('not-a-mime') → false --}}

Internationalization

Translation files ship for 21 locales out of the box:

ar · cs · da · de · en · es · fi · fr · it · ja · ko · nl · no · pl · pt · ro · ru · sv · tr · uk · zh

The package uses the zairakai:: namespace internally. Published views use the same namespace until you override the translation files.

# Publish translations to override
php artisan vendor:publish --tag=zairakai-lang

Translated keys:

// lang/vendor/zairakai/en/layout.php
return [
    'medias' => [
        'video'  => 'Your browser does not support the video element.',
        'audio'  => 'Your browser does not support the audio element.',
    ],
    'select' => [
        'default' => '-- Select an option --',
    ],
];

Testing

# Run test suite
composer test

# With coverage report
composer test:coverage

# Run BATS shell tests
bats tests/bats/unit/package-structure.bats
bats tests/bats/integration/composer-scripts.bats

# Full quality pipeline
make quality

Contributing

  1. Fork the repository on GitLab
  2. Create a feature branch: git checkout -b feat/my-component
  3. Ensure make quality passes (PHPStan max, Pint, Rector, ShellCheck)
  4. Commit following Conventional Commits: feat(scope): description
  5. Open a Merge Request

License

MIT — see LICENSE for details.

Support

Built by Stanislas Poisson