Loading...

This is taking longer than expected.

Coding standards

Blade

These are the standards the analyzer applies. Every rule states what it checks and why it exists.

All standards

Readable Markup

Blade and HTML must be written multi-line and indented. Do not emit minified markup: several elements collapsed onto one line, or a whole view or component written as one long line.

  • An element with children opens and closes on its own line, with the children indented one level.
  • Sibling elements go on separate lines.
  • Blade directives such as @if, @foreach, and @can open and close on their own lines, with the body indented.
  • An attribute list that is long — roughly three or more attributes, or a long Tailwind class list — may wrap one attribute per line.

A childless element with a short attribute list may stay on one line, such as <x-input wire:model="object.name" :label="__('Name')" />. Short inline content stays where it belongs — do not break <strong>, <a>, or {{ }} out of the paragraph or label that contains them just to satisfy this rule.
This rule governs markup being written or generated. Do not reformat existing untouched markup only to satisfy it. Templates under resources/views/vendor/, build output, and minified assets are out of scope.

{{-- Wrong --}}
<div class="flex flex-col gap-4"><x-input wire:model="object.name" :label="__('Name')" /><x-button type="submit">{{ __('Save') }}</x-button></div>

{{-- Correct --}}
<div class="flex flex-col gap-4">
    <x-input wire:model="object.name" :label="__('Name')" />
    <x-button type="submit">{{ __('Save') }}</x-button>
</div>

Prefer Project Form Components

Use project form components such as <x-input>, <x-select>, <x-textarea>, <x-checkbox>, <x-radio>, and other existing internal or WireUI form components instead of raw HTML form controls in application forms.
Avoid raw <input>, <select>, <option>, <textarea>, and similar controls when an equivalent project component exists. Project components provide consistent styling, labels, errors, accessibility, Livewire behavior, and validation presentation.
For date fields, use the project <x-date-input> component instead of raw HTML date inputs or generic inputs with type="date". The date component handles the project's timezone behavior consistently.
Raw HTML controls are allowed for hidden inputs, vendor-required markup, small browser-native controls with no project component equivalent, or cases where the existing component cannot support the required behavior. When using a raw control, the reason should be clear from nearby context.

{{-- Wrong --}}
<select wire:model="user.role">
    <option value="admin">{{ __('Admin') }}</option>
</select>

{{-- Correct --}}
<x-select wire:model="user.role" :options="$roleOptions" option-key-value />

{{-- Wrong --}}
<x-input type="date" wire:model="object.starts_at" />

{{-- Correct --}}
<x-date-input wire:model="object.starts_at" />

x-select Options Format

For local x-select options, use option-key-value with :options arrays.
Build that key/value array in the Livewire component or the view component that owns the data, and pass it to the template ready to render. Do not reshape the options inside the Blade file.

// Correct — the component shapes the options
public function getSelectOptions(): Collection
{
    return $this->getOptions()->mapWithKeys(function ($option) {
        return [$option => $option];
    });
}

For async selects using async-data or :async-data, option-label and option-value are allowed because the API response defines the label/value fields.

{{-- Correct --}}
<x-select wire:model="currency" :options="$currencyOptions" option-key-value />
<x-select :async-data="route('api.products.index')" option-label="name" option-value="id" wire:model.live="product" />

Livewire Form Submission

When a Blade or Livewire UI contains inputs that are submitted by one primary action, wrap those fields in a <form> and bind the Livewire action on the form with wire:submit or the project's established submit directive. The primary action must be a submit button so pressing Enter from an input submits the form.
Do not rely only on wire:click on the primary save/search/submit button for a real form, because that breaks normal keyboard submission. Secondary actions inside the form, such as cancel, close, add row, remove row, or local toggles, must use type="button" when they should not submit the form.
This rule does not apply to isolated buttons, links, tabs, dropdown toggles, pagination, instant filters that apply through live model updates without an explicit submit action, or UI controls that are not submitting a set of inputs.

{{-- Wrong --}}
<div>
    <x-input wire:model="object.name" :label="__('Name')" />
    <x-button wire:click="save">{{ __('Save') }}</x-button>
</div>

{{-- Correct --}}
<form wire:submit="save">
    <x-input wire:model="object.name" :label="__('Name')" />
    <x-button type="submit">{{ __('Save') }}</x-button>
    <x-button type="button" wire:click="closeModal">{{ __('Cancel') }}</x-button>
</form>

User Feedback And Error States

Interactive Blade and Livewire UI must give understandable feedback when an action cannot complete or input is invalid.

  • Forms must render validation errors near the relevant fields using project form components or the project's established error-display pattern.
  • Buttons or actions that can fail must surface a clear user-facing message through the existing feedback pattern, such as validation errors, WireUI notifications/dialogs, inline errors, or an existing alert component.
  • Elements that visually look like action buttons must perform an action, navigate, open/close UI, submit, dispatch an event, or clearly communicate why they are unavailable. If an element is only informational or decorative, do not style it like an actionable button; use text, badge, label, tooltip, disabled state, or another non-action pattern instead.
  • Report a button-like element as a UX error when it has no click/navigation/submission behavior and no clear unavailable/disabled semantics.
  • Do not leave a user action with no visible result when the operation is blocked, rejected, invalid, or fails server-side.
  • Error text must explain what happened or what the user needs to change. Generic messages such as Error, Invalid, or Something went wrong are not enough when the code knows the specific reason.
  • It is acceptable for purely local UI controls such as tabs, dropdown toggles, modal close buttons, filters, and pagination to avoid explicit success/error messages when they are not performing a risky or persistent operation.

Authorization in Livewire Components

Every Blade view that reads or mutates protected resources must enforce authorization when protected actions or protected data are present.

  • For protected mutating action buttons that exist in the view (delete, migrate, edit), prefer @can / @cannot directives when adding or changing authorization visibility
  • Equivalent existing project authorization wrappers are acceptable when they already delegate to policy-backed authorization; do not rewrite them just for style
  • Never duplicate policy logic inside the view. If the policy already checks a condition (e.g. hasSubscribers), do not re-check it in Blade — trust the policy
  • The @can wrapper controls visibility only; the Livewire component must still have its authoritative server-side guard at the component level, normally in mount() as defined in the Livewire standard
  • Do not require @can for non-mutating UI controls such as closeModal, cancel, tab switches, filters, pagination, or other purely local UI state actions.
  • Do not require an additional @can wrapper inside a dedicated Livewire modal/component view when the component itself authorizes the protected resource at mount/component level. The Blade rule guards visibility in parent/list views; it must not duplicate the component-level authorization rule for every button inside an already-authorized component.
{{-- Correct — policy handles all conditions internally --}}
@can('delete', $billingPlan)
    <x-button wire:click="deletePlan" ... />
@endcan

{{-- Incorrect — duplicates logic already inside the policy --}}
@can('delete', $billingPlan)
    @if (! $billingPlan->hasSubscribers)
        <x-button wire:click="deletePlan" ... />
    @endif
@endcan

Modals Must Use wire-elements/modal

Do not use WireUI modal markup in Blade. Report this rule only when the reviewed Blade contains <x-modal>.
Use wire-elements/modal (LivewireUI\Modal\ModalComponent) for modal screens. Ordinary Livewire markup such as wire:model, form inputs, buttons, closeModal, save, or confirm is not evidence for this rule.

{{-- Wrong --}}
<x-modal wire:model="showModal">
    ...
</x-modal>

{{-- Correct --}}
<button wire:click="$dispatch('openModal', { component: 'posts.create-post' })">
    New Post
</button>

Views Do Not Transform Data

A view renders what it was given. It does not query, filter, sort, group, or reshape.
Work done inside a template runs on every render, is invisible to the component's tests, and cannot be reused by anything else. Move it to the component — a #[Computed] property or a method that returns the prepared data — and let the template read the result.

{{-- Correct --}}
@foreach ($this->activeOrders as $order)

{{-- Incorrect --}}
@php $orders = Order::where('active', true)->get(); @endphp
@foreach ($orders->filter(fn ($o) => $o->total > 0)->sortBy('name') as $order)

Reading a value, calling a method that returns prepared data, and chaining that ends in a string — a label, a formatted date, a joined list — are all rendering, not transformation. What this rule reports is a chain that returns a collection or runs a query.

Icon-Only Controls Have an Accessible Name

A button or link whose only content is an icon must carry an accessible name: an aria-label, aria-labelledby or title on the control or on the icon inside it, a <title> inside the SVG, or translated text inside a sr-only element. Without one, a screen reader announces the control as "button" or "link" with nothing to say what it does, and the person relying on it cannot tell delete from edit.

  • A native <button> or <a> showing only an SVG, an icon component, an icon-font <i> or an @svg or @icon directive, with no name visible in the same file, is an error.
  • An <x-button> or <x-mini-button> with only an icon and no label is a warning: whether the component renders a name from its icon is decided in its own file.
  • A native <button> or <a> that spreads {{ $attributes }} takes its name from whoever renders it, so it is a warning that asks for every caller to pass one.

The name is copy like any other and goes through translation.

{{-- Wrong --}}
<button wire:click="delete({{ $item->id }})">
    <x-icon name="trash" class="h-4 w-4" />
</button>

{{-- Correct --}}
<button wire:click="delete({{ $item->id }})" aria-label="{{ __('Delete') }}">
    <x-icon name="trash" class="h-4 w-4" />
</button>

Visible Text Goes Through Translation

Text a person reads in a view — between tags, or in an attribute a person reads or hears such as alt, title, placeholder, aria-label, label, description, hint or tooltip — is rendered with __(), with its entry added to every locale file as the PHP Language & Text rule describes. That rule reports non-English copy rendered to a person; this one is the separate translation concern that rule leaves out, and reports text written straight into a view, English included, so the two can report the same non-English copy. Text written straight into the markup ships in one language whatever the visitor chose, and nobody notices until a person using another language opens the page.
This is a warning, because a plain string is sometimes a name that stays the same in every language. Numbers, symbols, acronyms, addresses, code names, escaped markup, content that comes from a variable, and text inside scripts, styles, code samples and icon glyphs are not reported. Text made only of capitalised words is reported only when one of them is an ordinary interface word such as Dashboard or Close, since otherwise it may be a brand or a person's name.

{{-- Wrong --}}
<h1>Dashboard</h1>
<img src="{{ $logo }}" alt="Company logo">

{{-- Correct --}}
<h1>{{ __('Dashboard') }}</h1>
<img src="{{ $logo }}" alt="{{ __('Company logo') }}">

Plurals Come From the Translation Files

A word that changes with a count is chosen with trans_choice() and a plural entry in the locale files, never by comparing the count in the view or with Str::plural(). A ternary on === 1 assumes every language has exactly one singular and one plural, which is wrong for languages with three or more forms, and Str::plural() applies one language's endings — English unless configured otherwise — to whatever word it is given, so a Spanish canción comes out as cancións.
This is a warning: a comparison against one that chooses between two unrelated texts is not a plural and is left alone.

{{-- Wrong --}}
{{ $count }} {{ $count === 1 ? __('item') : __('items') }}
{{ $count }} {{ Str::plural('item', $count) }}

{{-- Correct --}}
{{ trans_choice('{0} No items|{1} :count item|[2,*] :count items', $count) }}

The string is the translation key: add it to every locale file with the forms that language needs.

Weblabor Logo Developed by Weblabor