---
title: Strict Validation
description: How WireKit validates component prop values across CLI, HTTP, and test contexts — and how to opt into the strictest mode.
visibility: guest
draft: false
related:
  - /variants-and-intents
  - /customization
  - /cli-reference
---

# Strict Validation

Every WireKit component with a prop enum (`<x-wirekit::button intent="primary">`, `<x-wirekit::alert variant="info">`, `<x-wirekit::hero size="lg">`, …) routes invalid values through a single decision point: the **StrictnessGate**. The gate decides whether to throw, log, and which value to render when a developer typo lands in production code.

The decision depends on the runtime context. That is intentional — a typo in a Pest test should fail fast and visibly; the same typo on a live HTTP request should not 500 the whole page over a cosmetic glitch. This page documents the full matrix so you can decide which mode fits each environment.

## The decision matrix

Two configuration knobs interact with the runtime context:

| Knob | Default | What it does |
|---|---|---|
| `wirekit.validation.strict` | `null` (auto-detect from `APP_DEBUG`) | When `null`, the gate is **strict** if `APP_DEBUG=true`, **lenient** otherwise. Set explicitly to `true` or `false` to override the auto-detect. |
| `wirekit.validation.throw_on_invalid` | `false` | When `true`, the gate throws even in HTTP requests under strict mode. Useful for staging environments that should fail loudly. |

The resulting behavior:

| Runtime context | `strict=true` (or `APP_DEBUG=true`) | `strict=false` (or `APP_DEBUG=false`) |
|---|---|---|
| **HTTP request** | log `ERROR` + render fallback | log `WARNING` + render fallback |
| **CLI / Pest / `artisan tinker`** | **throw** `InvalidArgumentException` | log `WARNING` + render fallback |
| `throw_on_invalid=true` (any context) | **throw** `InvalidArgumentException` | **throw** `InvalidArgumentException` |

**Fallback semantics:** when the gate renders a fallback value, it picks the **first allowed enum entry**. For `<x-wirekit::button surface="weird-typo">` the fallback is `surface="filled"`. Each component can override the fallback per prop; the override surfaces in the same log line.

## Why the split exists

A naive "throw on any typo" policy looks safe in isolation but creates a worse production failure mode: one cosmetic prop typo (a developer wrote `surface="outlined"` instead of `surface="outline"`) takes down the entire Blade view with a 500 error. The page that should have rendered with a slightly-wrong button now renders nothing at all.

The matrix above resolves the trade-off:

- **In a Pest test**, the throw surfaces the typo at the assertion that should have caught it. No silent log line; no rendered fallback the test then accidentally validates.
- **In `php artisan tinker`**, the throw matches what a developer would expect from a debugger — you typed the wrong thing, you get a stack trace.
- **In a live HTTP dev request** (`APP_DEBUG=true`), the page renders with the fallback so you can see the rest of the layout, and the `local.ERROR: WireKit [...]` line tells you exactly what went wrong.
- **In production**, the log line drops to `WARNING` (less noisy) but the fallback still keeps the page alive.

## Spotting silent typos in logs

Two options:

**Option 1 — let `wirekit:doctor` scan the log for you.** The doctor's environment-tier checks include an optional silent-typo log scan that walks `storage/logs/laravel*.log` for the `WireKit [...]` ERROR / WARNING lines emitted by StrictnessGate. A WARN with example lines surfaces every typo that shipped:

```bash
# 1. The environment-tier checks include the log scan.
php artisan wirekit:doctor --tier=environment
```

The scan SAFE-DEGRADES at every failure mode — missing log file, custom log channel (Slack / Papertrail / Sentry), log-level filtering below WARNING — it surfaces an INFO line explaining the skip and never blocks the doctor's exit code. Disable entirely by setting `wirekit.doctor.scan_logs` to `false` (or `WIREKIT_DOCTOR_SCAN_LOGS=false` in `.env`) when your app doesn't write file-based logs.

**Option 2 — grep the log directly.** Lenient mode logs at `WARNING` level; strict-HTTP logs at `ERROR`. Either way the lines look the same:

```bash
# 1. From your project root, scan today's log for any WireKit fallback events.
grep 'WireKit \[' storage/logs/laravel.log

# 2. Narrow to one component if you suspect a specific surface.
grep 'WireKit \[button\]' storage/logs/laravel.log

# 3. Tail the log live while you click through the app to catch typos as they fire.
tail -f storage/logs/laravel.log | grep --line-buffered 'WireKit \['
```

A typical line looks like:

```text
[2026-05-28 06:54:53] local.ERROR: WireKit [button]: Invalid surface "outlined". Allowed: filled, outline, soft, ghost, link. Falling back to "filled".
```

The `Falling back to "..."` suffix is the source of truth for what the page actually rendered.

## Opting into strict mode for staging

Staging environments typically run with `APP_DEBUG=false` to mirror production, which puts the gate into lenient mode. If you would rather fail loudly on a typo before it reaches end users, flip the two knobs explicitly in your staging `.env`:

```bash
# 1. Force the gate into strict mode regardless of APP_DEBUG.
WIREKIT_STRICT_VALIDATION=true

# 2. Force a throw on every invalid value — no fallback, full stack trace.
WIREKIT_THROW_ON_INVALID=true
```

Add the corresponding config wiring to `config/wirekit.php`:

```php
<?php

// 1. The 'validation' block lives at the top level of the wirekit config.
return [
    // …
    'validation' => [
        // 2. null = auto-detect from APP_DEBUG. true/false = explicit override.
        'strict' => env('WIREKIT_STRICT_VALIDATION'),
        // 3. When true, the gate throws even in HTTP — page 500s on invalid props.
        'throw_on_invalid' => env('WIREKIT_THROW_ON_INVALID', false),
    ],
    // …
];
```

::: warning
Be careful with `throw_on_invalid=true` in production — every invalid prop value will 500 the request that triggered it. The setting is useful in CI's smoke-test job and dangerous in front of real visitors. You must leave it `false` in production unless you have a deliberate fail-loudly policy.
:::

## Testing strict mode in Pest

CLI context already triggers the throw branch when strict mode is active. A typical regression test reads:

```php
<?php

// 1. Pest browser test asserting an invalid prop value throws under strict mode.
use Illuminate\Support\Facades\Blade;

it('rejects unknown surface values on <x-wirekit::button>', function () {
    // 2. Force strict mode explicitly so the test is independent of APP_DEBUG.
    config()->set('wirekit.validation.strict', true);

    // 3. The render call throws because 'outlined' is not in the surface enum.
    expect(fn () => Blade::render('<x-wirekit::button surface="outlined">x</x-wirekit::button>'))
        ->toThrow(InvalidArgumentException::class, 'Invalid surface "outlined"');
});
```

The exception message carries a Levenshtein-ranked Did-you-mean hint — `Did you mean "outline"?` — so the failing assertion already points at the likely correct value.

## How `intent` and `surface` interact under the gate

Components like `<x-wirekit::button>` and `<x-wirekit::badge>` carry two orthogonal enums (`intent` + `surface`). The gate runs each prop independently — an invalid `intent` falls back to `primary`, an invalid `surface` falls back to `filled`, and a value that lands outside both enums logs two separate lines. See [Variants & Intents](/variants-and-intents) for the full prop-value catalog.

## Asking the gate instead of reading its log

The gate's warnings are written for a developer reading a log. Two of the questions behind
them are also available as plain predicates you can assert on in your own test suite —
useful when you want a build to fail rather than a line to appear.

Both return an answer and nothing else: no logging, and no `app.debug` gate. That matters,
because the warnings themselves return early outside debug mode, so in production the
signal does not exist at all and no test can reach it.

```php
use Pushery\WireKit\Support\StrictnessGate;

// Attribute names that are neither declared props nor legitimate HTML passthrough.
StrictnessGate::unknownPropNames($attributes, $declaredProps);   // => ['varaint']

// Scope directives this component will DISCARD, because it sets its own on the
// same element and HTML keeps the first of two identical attributes.
StrictnessGate::discardedScopeDirectives('dropdown', ['x-data' => '{ open: false }']);
// => ['x-data']
```

`discardedScopeDirectives()` reads the component's own Blade template and asks whether it
sets the directive **unconditionally** — a component that only sets `x-data` inside a
condition does not always discard yours, and answering otherwise would report a collision
that does not happen. That distinction is why asking WireKit is better than re-deriving the
answer from the templates yourself.

::: warning
A discarded `x-data` is not a degradation, it is a disconnection. Your scope never comes
into existence, so everything written against it — an `x-init` beside it, a `@click` inside
it — silently resolves against the component's own data. Wrap the component in your own
element, or use the API the component already exposes.
:::

## See Also

- [Variants & Intents](/variants-and-intents) — canonical prop-value reference
- [Customization](/customization) — overriding component defaults via config
- [CLI Reference](/cli-reference) — `wirekit:doctor` and the wider Artisan surface
