Masked Input
Masked credential field with reveal and copy actions, built on the input-group primitive.
Preview
<x-ui.masked-input value="sk-live-9f2c4ade71b0demo" label="API key" class="mx-auto w-full max-w-md" />
Installation
php artisan ui:add masked-input
Note
This component ships an Alpine behavior module at
resources/js/ui/masked-input.js. Import it once from your bundle so it registers on alpine:init:
import './masked-input.js';
Registry contract
php artisan ui:add masked-input
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/masked-input.blade.php -
resources/js/ui/masked-input.js
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Masked Input (`masked-input`)
Masked credential field with reveal and copy actions, built on the input-group primitive.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add masked-input
```
## Usage
```blade
<x-ui.masked-input value="sk-live-9f2c4ade71b0demo" label="API key" class="mx-auto w-full max-w-md" />
```
## Props
- `value` (string, default `tok_a1b2c3d4e5f6g7h8`) — The secret value shown once revealed and written to the clipboard when copied.
- `label` (string, default `Secret`) — Visible label shown above the field.
## Use when
- Use when formatting, validation, or constrained entry meaningfully reduces errors, such as OTP, phone, currency, password, or numeric entry.
- Displaying a sensitive read-only value, such as an API key or token, hidden by default with an explicit reveal action.
- Pairing that value with a one-click copy action without ever showing it in plain text unintentionally.
## Avoid when
- Do not add specialized behavior if a plain field would be simpler and more forgiving.
- The value must be editable; compose input-group directly instead.
- The value is not sensitive and can be shown by default; use input-copy instead.
## Anti-patterns
- Adding formatting behavior that makes correction harder
## Rules
- Use the `<brok:masked-input>` tag (or `<x-ui.masked-input>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/masked-input
- Registry JSON (files, props, contract): https://brokui.dev/r/open/masked-input.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | tok_a1b2c3d4e5f6g7h8 | The secret value shown once revealed and written to the clipboard when copied. |
| label | string | Secret | Visible label shown above the field. |
Slots
default— Unused; the field renders from props only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders read-only over input-group; the field type toggles between password and text via the eye button.
- The eye button reflects its state with aria-pressed; the copy button writes the value to the clipboard and briefly swaps its icon and label to a copied state.
- The value lives in one Alpine state, so the read-only field and clipboard copy share a single source of truth.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.
Guidance
Collect constrained information with error-reducing formatting.
Use when
- Use when formatting, validation, or constrained entry meaningfully reduces errors, such as OTP, phone, currency, password, or numeric entry.
- Displaying a sensitive read-only value, such as an API key or token, hidden by default with an explicit reveal action.
- Pairing that value with a one-click copy action without ever showing it in plain text unintentionally.
Avoid when
- Do not add specialized behavior if a plain field would be simpler and more forgiving.
- The value must be editable; compose input-group directly instead.
- The value is not sensitive and can be shown by default; use input-copy instead.
Use instead
- Plain text or number field with validation
Anti-patterns
- Adding formatting behavior that makes correction harder
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- managed
- Focus
none
- The reveal and copy buttons each carry an aria-label that changes with state ("Show value"/"Hide value", "Copy value"/"Copied"), never colour alone.
- The field is announced via aria-label from the label prop even though it is read-only.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="masked-input-{{ $record->id }}">
<x-ui.masked-input value="sk-live-9f2c4ade71b0demo" label="API key" class="mx-auto w-full max-w-md" />
</div>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'value' => 'tok_a1b2c3d4e5f6g7h8',
'label' => 'Secret',
])
@php
$config = ['value' => (string) $value];
@endphp
{{--
Masked input — a masked credential field over <x-ui.input-group>. The input
type toggles between password and text via `revealed`; the eye button
reflects state with aria-pressed; the copy button writes the value to the
clipboard and flips a transient `copied` flag that swaps the icon and label.
The value lives in Alpine state so the read-only field and clipboard copy
share one source of truth.
--}}
<div
data-slot="masked-input"
x-data="uiMaskedInput(@js($config))"
{{ $attributes->merge(['class' => 'flex flex-col gap-2 text-foreground']) }}
>
<x-ui.label :for="false" x-bind:for="$id('masked-input-field')">{{ __($label) }}</x-ui.label>
<x-ui.input-group>
<x-ui.input-group.input
x-bind:id="$id('masked-input-field')"
x-bind:type="revealed ? 'text' : 'password'"
x-model="value"
readonly
autocomplete="off"
spellcheck="false"
:aria-label="__($label)"
/>
<x-ui.input-group.button
x-on:click="toggle()"
x-bind:aria-pressed="revealed.toString()"
x-bind:aria-label="revealed ? '{{ __('Hide value') }}' : '{{ __('Show value') }}'"
>
{{-- Eye (hidden state). --}}
<svg x-show="!revealed" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
{{-- Eye-off (revealed state). --}}
<svg x-show="revealed" x-cloak aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.9 4.2A10.6 10.6 0 0 1 12 4c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.3 4.2M6.6 6.6A17.6 17.6 0 0 0 2 11s3.5 7 10 7a10.6 10.6 0 0 0 4.1-.8" />
<path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
<path d="m2 2 20 20" />
</svg>
</x-ui.input-group.button>
<x-ui.input-group.button
x-on:click="copy()"
x-bind:aria-label="copied ? '{{ __('Copied') }}' : '{{ __('Copy value') }}'"
>
{{-- Clipboard (idle). --}}
<svg x-show="!copied" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
</svg>
{{-- Check (copied). --}}
<svg x-show="copied" x-cloak aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success">
<path d="M20 6 9 17l-5-5" />
</svg>
<span class="sr-only" x-text="copied ? '{{ __('Copied') }}' : '{{ __('Copy value') }}'"></span>
</x-ui.input-group.button>
</x-ui.input-group>
</div>
/**
* Masked input behavior.
*
* Holds the credential `value`, the `revealed` masking flag, and a transient
* `copied` flag. `toggle()` flips masking; `copy()` writes the value to the
* clipboard via navigator.clipboard and resets `copied` after a short delay so
* the check icon and "Copied" label are momentary. Guards the clipboard call so
* insecure/unsupported contexts fail quietly. Self-registers on `alpine:init`.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiMaskedInput', (config = {}) => ({
value: config.value ?? '',
revealed: false,
copied: false,
_timer: null,
toggle() {
this.revealed = !this.revealed;
},
async copy() {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(this.value);
}
this.copied = true;
clearTimeout(this._timer);
this._timer = setTimeout(() => {
this.copied = false;
}, 1500);
} catch (error) {
// Clipboard unavailable (insecure context / denied permission).
console.warn('masked-input copy failed:', error);
}
},
destroy() {
clearTimeout(this._timer);
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
2.0.5 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0