Skip to main content
Copy for LLM

Customization

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

Customization vs. Theming vs. Design Tokens. Theming is the setup guide + theme-preset gallery — how to wire tokens into your app.css, the eight copy-paste presets (Default, Minimal, Soft, Material, Brutalist, Retro Terminal, Cupertino, Aurora), and the accessibility guarantees. 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 for the full setup walkthrough and Design Tokens for the complete variable reference.

/* 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():

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],
]);
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:

use Pushery\WireKit\WireKit;

// 1. What THIS call registered — a record of the write, not the value in effect.
$registered = WireKit::defaultsFor('button');
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.

The same thing, straight in the config file

WireKit::defaults() writes into config/wirekit.php, so you can also write it there and skip the boot code entirely. This is the form every component page's Customization section shows:

// config/wirekit.php
'components' => [
    'button'  => ['intent' => 'neutral', 'surface' => 'outline', 'size' => 'lg'],
    'tooltip' => ['placement' => 'bottom', 'delay-show' => 500],
],

Reach for the service-provider call when the value has to be computed at boot (a tenant's brand intent, an environment switch); reach for the file when it is a constant, because a published config file survives config:cache and a closure does not.

Which keys a component accepts is not a separate list to look up. A prop is settable this way when its declared default reads through config() — which is exactly what the Default column of that component's props table shows. Multi-word keys are kebab-case (delay-show, max-tags, native-on-mobile), matching the config file rather than the camelCase prop name.

A sub-component's prop sits under its parent. A key with a dot in the config file ('dropdown.panel') carries only classes. The prop default of a sub-component is read through its parent, so in the file it nests there:

// config/wirekit.php
'components' => [
    'dropdown' => ['panel' => ['width' => 'trigger']],
],

WireKit::defaults() accepts 'dropdown.panel' => ['width' => 'trigger'] as well and writes the value to the same place.

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:

WireKit::personalize('card', [
    'base' => 'bg-[var(--color-wk-bg)] text-[color:var(--color-wk-text)] rounded-none overflow-hidden',
]);

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.

A few classes stay whatever you replace. The markers a component needs to work or to be usable, such as the overlay layer of a modal, the scrollbar styling of a scroll container, the touch target of a small button or the width of a carousel slide, are set in front of the block rather than inside it, so owning a block's look never takes them away.

The string is the whole block, so it has to carry everything the block did. A card's block is its surface and its corners. The block of something you can operate holds more: a button's also carries its focus ring, its disabled state and the wk-button marker, and a string that leaves them out ships a button a keyboard user cannot see focus on. For a change to part of a block, use the closure form below.

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:

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:

WireKit::personalize('button', [
    'base' => fn (string $vendor): string => str_replace('font-[number:var(--font-wk-body-weight)]', 'font-bold', $vendor),
]);

A swap only does something when the class it names is in the block, so name it as the block ships it. To see the block, log $vendor from a closure that returns it unchanged.

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.

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.

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:

// 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. 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:

WireKit::scope('pill', [
    'button' => ['classes' => ['base' => 'rounded-full']],
    'input'  => ['classes' => ['base' => 'rounded-full']],
]);
<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:

// 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)]',
    ]],
]);

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.

The weight is the one part you can change without taking over the slot. A selected segment reads --font-wk-selected-weight, a Level 1 token, so this keeps it in regular type while its surface, text color and shadow stay the component's own:

:root {
    --font-wk-selected-weight: var(--font-wk-body-weight);
}

Design Tokens lists the components that read it.

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:

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.

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
  • Chart Adapters: Configure chart libraries via config — see Chart docs
  • Font Presets: Configure locally bundled fonts — see Fonts docs

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store