---
title: Customization
description: Four levels of customization — CSS variables, PHP defaults, deep personalization scopes, and view publishing
visibility: guest
draft: false
---

# Customization

WireKit offers 4 levels of customization. Use the lightest level that solves your need.

> **Customization vs. Theming vs. Design Tokens.** [`Theming`](/theming) is the **setup guide + theme-preset gallery** — how to wire tokens into your `app.css`, the seven copy-paste presets (Default, Minimal, Soft, Material, Brutalist, Retro Terminal, Cupertino), and the accessibility guarantees. [`Design Tokens`](/theming/design-tokens) is the **complete variable reference** — every `--color-wk-*` / `--radius-wk-*` / `--shadow-wk-*` plus the reading-* component family and chart palette. **Customization** (this page) is the **decision tree** — when to override tokens vs. tweak PHP defaults vs. apply scope-based personalization vs. publish the view.

## Level 1: CSS Variables

Change colors and radii without touching PHP. See [Theming](/theming) for the full setup walkthrough and [Design Tokens](/theming/design-tokens) for the complete variable reference.

```css
/* In your app.css, after @import 'tailwindcss'; */
:root {
    --color-wk-accent: var(--color-blue-600);
    --radius-wk-md: 0.75rem;
}
.dark {
    --color-wk-accent: var(--color-blue-400);
}
```

**Use this level when:** every component should pick up the same change (e.g. brand accent color, global border radius). The override cascades on top of the separately-loaded `wirekit.css`. No `@layer base` wrapper is needed — the `--color-wk-*` tokens are plain CSS custom properties, not Tailwind theme tokens.

### Using WireKit tokens in your own Tailwind classes

You can reference any `--*-wk-*` token inside a Tailwind v4 arbitrary-value utility — but Tailwind needs a **type hint** so it compiles the value:

- **Color:** `text-[color:var(--color-wk-text-muted)]`, `bg-[color:var(--color-wk-bg-subtle)]`
- **Length** (font size, ring width): `text-[length:var(--text-wk-sm)]`
- **Spacing / radius** (untyped): `p-[var(--padding-wk-x-lg)]`, `rounded-[var(--radius-wk-md)]`

Omit the `color:` / `length:` hint and Tailwind v4 mis-types the arbitrary value and silently drops the class. And because Tailwind only generates classes it has scanned, a newly-authored arbitrary-value class appears only after the dev server re-scans — restart `npm run dev` (or run a one-shot `npm run build`) after adding one. When a WireKit component already carries the token, prefer the component over a hand-written token class.

## Level 2: PHP Defaults

Set project-wide defaults for every component — no view publishing needed.

In your `AppServiceProvider::boot()`:

```php
use Pushery\WireKit\WireKit;

WireKit::defaults([
    'button'   => ['intent' => 'neutral', 'surface' => 'outline', 'size' => 'lg'],
    'input'    => ['size' => 'lg'],
    'dropdown' => ['placement' => 'bottom-end', 'offset' => 4],
    'modal'    => ['size' => 'lg', 'dismissible' => true],
    'drawer'   => ['position' => 'left', 'size' => 'md'],
    'tooltip'  => ['placement' => 'bottom', 'delay-show' => 500],
]);
```

::: warning Every key must be a prop the component declares
An unknown one does not fail — Blade folds it into the attribute bag, where it renders as a literal HTML attribute that nothing reads, so the page looks finished. `button` is the trap worth naming: it has `intent` and `surface`, never `variant`.

In development WireKit logs a warning naming the component, the unrecognized name and the closest declared prop, so a typo surfaces instead of rendering as a silent no-op. It is written to your log channel, not to the page, and it is skipped entirely in production. Valid HTML attributes and framework wiring (`aria-*`, `data-*`, `wire:`, `x-`, `v-`) are never flagged. Check the component's own page when you are unsure.
:::

**Reading back what you set.** `WireKit::defaultsFor('button')` returns the array registered for
one component, and an empty array when nothing was registered for it:

```php
use Pushery\WireKit\WireKit;

// 1. What THIS call registered — a record of the write, not the value in effect.
$registered = WireKit::defaultsFor('button');
```

::: warning It reports what was registered, not what is in effect
A default can also come from the published config file, and `WireKit::defaults()` writes into that
same config so both routes end up in one place. `defaultsFor()` reads only the runtime record, so a
component styled entirely from `config/wirekit.php` returns an empty array here while rendering with
defaults. Do not read an empty array as “this component has no default” — read
`config('wirekit.components')` for the value that is actually in effect.
:::

**Use this level when:** a specific component prop should have a different default project-wide (e.g. every button is `size="lg"`). Tokens (Level 1) change the look; defaults (Level 2) change the behavior. Both are global; both can coexist.

## Level 3: Deep Personalization

Replace entire CSS class blocks per component:

```php
WireKit::personalize('button', [
    'base' => 'inline-flex items-center justify-center font-semibold tracking-wide',
]);
```

**Use this level when:** the change is structural (the layout / spacing / typography of a specific component slot) and a token override can't express it. Personalization replaces the WireKit-supplied class string for that slot wholesale, so you take ownership of the slot's structural CSS.

### Adjusting a block instead of replacing it

Taking ownership of a slot has a cost that shows up much later: from that point the block is yours, and every subsequent improvement WireKit makes to it stops reaching your app. Nothing is broken, so the override simply looks like a decision you made, for as long as it stands.

`php artisan wirekit:doctor` names them for you. It reports every block whose value is a finished class string — the shape that replaces — and stays silent when every block extends. That puts the trade in front of you on the day you go looking, rather than on the day an upstream change quietly failed to arrive. If you want the same list in your own tooling, `WireKit::personalizedComponents()` returns it.

When your change is a delta rather than a rewrite, give the block a **closure** instead of a string. It receives the class string WireKit ships and returns the one you want:

```php
WireKit::personalize('sidebar.item', [
    // 1. $vendor is the block exactly as WireKit ships it, this version.
    // 2. Return what you want rendered — here, everything plus one change.
    'base' => fn (string $vendor): string => $vendor.' rounded-none',
]);
```

You are not limited to appending. The point is that you can see what you are overriding, so you can also remove or swap one part and keep the rest:

```php
WireKit::personalize('button', [
    'base' => fn (string $vendor): string => str_replace('font-medium', 'font-bold', $vendor),
]);
```

Both forms work on `personalize()` and on `scope()`. A plain string still replaces the block outright — that behavior is unchanged, and it is still the right choice when you genuinely want to own the slot.

::: tip
Prefer the closure form whenever your change is small. It keeps you on the upgrade path: a new utility WireKit adds to that block arrives in `$vendor` and flows through, while your delta stays applied.
:::

::: warning
This applies to `personalize()` and `scope()`, not to the config-file overrides below. A closure in a config file cannot survive `php artisan config:cache`, so a personalization written there would work in development and vanish on the first cached deploy.
:::

### Class overrides via config

The same class blocks can also be set straight in `config/wirekit.php` under `components.{name}.classes.{block}` — the lowest-friction override, with no service-provider boot code:

```php
// config/wirekit.php
'components' => [
    'button' => [
        'classes' => ['base' => 'inline-flex items-center rounded-full'],
    ],

    // Sub-components use their LITERAL dotted name as the key — 'sidebar.item',
    // 'card.header', 'table.th', 'dropdown.panel'. Keep the dot IN the key;
    // do NOT nest it as 'sidebar' => ['item' => [...]].
    'sidebar.item' => [
        'classes' => ['active' => 'bg-[var(--color-wk-accent)] text-[color:var(--color-wk-accent-fg)]'],
    ],
],
```

Config overrides sit below `personalize()` and `scope()` in the priority chain (deep > scoped > config > default), so a `personalize()` call always wins over a config entry for the same block.

> **Heads-up — class strings authored in PHP or config are outside Tailwind's content scan.** A utility that appears *only* inside a `config/wirekit.php` value or a `personalize()` call (never in a Blade template) is not generated unless you point Tailwind at that file with an `@source` line in your CSS — see the Source Scanning step in the [Integration guide](/getting-started/integration). Without it the override resolves to a class name that has no matching CSS, and the change silently does nothing.

### Scoped Personalization

Apply personalization only to specific instances:

```php
WireKit::scope('pill', [
    'button' => ['classes' => ['base' => 'rounded-full']],
    'input'  => ['classes' => ['base' => 'rounded-full']],
]);
```

```blade
<x-wirekit::button scope="pill">Pill Button</x-wirekit::button>
<x-wirekit::button>Normal Button</x-wirekit::button>
```

### State-Dependent Slots

Most slots describe a component's resting appearance. A few describe how it
looks in a particular *state* — and those are worth calling out, because they
are what you need when a brand styles the selected thing differently rather
than just recoloring the whole control.

`segmented-control` exposes both branches of its selection state:

```php
// 1. Restyle the selected segment — e.g. a brand fill instead of the
//    default elevated pill.
WireKit::scope('brand-tabs', [
    'segmented-control' => ['classes' => [
        'segment-selected' => 'bg-[var(--brand-gold)] text-[var(--brand-navy)] font-semibold',
        'segment-unselected' => 'text-[var(--brand-navy)]/60 hover:text-[var(--brand-navy)]',
    ]],
]);
```

::: warning
A state slot is replaced wholesale, never merged — so your override must
describe the state's *full* appearance: background, text color, elevation and
weight. Setting only a background silently drops the rest, leaving the selected
segment with no text-color or shadow treatment at all.
:::

Not every component with reactive state offers these slots yet. Where the
appearance is still resolved inside the runtime binding, a per-instance
override cannot reach it and a global token override is the way to change it.

## Level 4: Publish Views

Full ownership — edit the Blade templates directly:

```bash
php artisan vendor:publish --tag=wirekit-views
```

Views land in `resources/views/vendor/wirekit/components/` and take priority over the package views.

**Use this level when:** Levels 1–3 can't express the change (entirely new DOM structure, custom slots, custom Alpine wiring). Publishing is the heaviest tool and the only one that survives a custom-Blade-author's full creative control. Avoid using it as a shortcut for what Levels 1–3 can already do — see the warning below.

::: warning
Published views are detached from updates: when WireKit releases a new version, published views never receive changes automatically. Avoid publishing more than you actually need to modify, or you must hand-merge upstream improvements on every release.
:::

## Additional Configuration

Beyond the 4-level component customization, WireKit offers config-level settings for icons and charts:

- **Icon Presets:** Switch icon sets globally via `config/wirekit.php` — see [Icon docs](/components/icon)
- **Chart Adapters:** Configure chart libraries via config — see [Chart docs](/components/chart)
- **Font Presets:** Configure locally bundled fonts — see [Fonts docs](/components/fonts)
