Use it for
Use BbColorInput when color is an editable form value with a label, validation, and a readable hex representation.
Use something else when
BbColorPalette— color is assigned from a trigger you ownBbSelect— users must choose from a closed palette
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
Edit a label color
Bind a seeded string | null model. The field emits only a complete lowercase #RRGGBB value, or null when cleared.
<template>
<div class="flex max-w-sm flex-col gap-3">
<!-- Repaints on every drag frame — one style binding, no watcher. -->
<span
class="self-start rounded-full px-2.5 py-0.5 text-xs font-medium"
:style="{
backgroundColor: color ?? 'var(--bb-muted)',
color: textOn(color),
}"
>
{{ name || 'label preview' }}
</span>
<BbTextInput
id="label-name"
v-model="name"
label="Label name"
name="label-name"
placeholder="needs-triage"
/>
<BbColorInput
id="label-color"
v-model="color"
label="Label color"
name="label-color"
swatches
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbColorInput, BbTextInput } from 'bitboss-ui';
const name = ref<string | null>('regression');
const color = ref<string | null>('#b91c1c');
/*
* The model is always a complete `#RRGGBB`, so this parses without a guard
* for a half-typed value — there is no such state to guard against.
*/
const textOn = (hex: string | null) => {
if (hex === null) return 'var(--bb-text)';
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.6 ? 'var(--bb-text)' : 'var(--bb-panel)';
};
</script>
Partial text never reaches the model. Convert rgb() or hsl() at your data boundary, and pass name when a native form post must include the value.
Picker options and limits
Add picker features together when the task needs them; they do not define separate field variants.
<template>
<div class="flex max-w-sm flex-col gap-4">
<!-- `swatches` on its own: the built-in grid, six hues by five shades. -->
<BbColorInput
id="swatches-brand"
v-model="brand"
label="Brand color"
name="brand-color"
swatches
/>
<!-- Custom presets: each inner array is a COLUMN, one hue per column. -->
<BbColorInput
id="swatches-label"
v-model="labelColor"
label="Label color"
name="label-color"
:swatches="SWATCH_COLUMNS"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbColorInput } from 'bitboss-ui';
const SWATCH_COLUMNS: string[][] = [
['#7f1d1d', '#b91c1c', '#ef4444'],
['#78350f', '#b45309', '#f59e0b'],
['#14532d', '#15803d', '#22c55e'],
['#1e3a8a', '#1d4ed8', '#3b82f6'],
['#4c1d95', '#6d28d9', '#8b5cf6'],
];
const brand = ref<string | null>('#4f46e5');
const labelColor = ref<string | null>('#b91c1c');
</script>
swatchestakestrueorstring[][]; each inner array is a column.alphachanges both the picker and model to#RRGGBBAA. Without it, an eight-digit input cannot round-trip.eye-dropperis progressive enhancement. Its button is absent in Safari and Firefox, and sampled colors are opaque.
Coming from v2picker → eye-dropper
picker became eye-dropper. The old attribute falls through to $attrs and silently does nothing.
Validation and locked values
Validate presence and business rules, not hex syntax: the mask already owns the format.
<template>
<div class="flex max-w-sm flex-col gap-4">
<BbColorInput
id="chrome-brand"
v-model="brand"
clearable
description="Used for primary buttons and links across the app."
:errors="brandErrors"
hint="Clearing the field sets the model back to null."
label="Brand color"
name="brand-color"
required
swatches
/>
<!-- readonly: readable, focusable, copyable — and the dot never opens. -->
<BbColorInput
id="chrome-imported"
label="Imported from the old theme"
:model-value="importedColor"
name="imported-color"
readonly
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { BbColorInput } from 'bitboss-ui';
const brand = ref<string | null>(null);
const importedColor = '#0e8a16';
/*
* The mask already guarantees the shape, so presence is all there is left to
* check on the client. A "that is not a color" rule could never fire.
*/
const brandErrors = computed(() =>
brand.value === null ? ['Pick a brand color before saving.'] : []
);
</script>
readonly keeps an imported color focusable and copyable; disabled removes it from the tab order. Both block the picker. clearable emits null.
Application-owned persistence
Picker drags emit on every frame. Repaint a preview directly from the model, but debounce remote persistence or save on blur or form submit.
Model: #4f46e5
<template>
<div class="flex max-w-sm flex-col gap-3">
<BbColorInput
id="brand-color"
v-model="brand"
label="Brand color"
name="brand-color"
placeholder="#RRGGBB"
/>
<p class="text-sm opacity-70">
Model: <code>{{ brand === null ? 'null' : brand }}</code>
</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbColorInput } from 'bitboss-ui';
/*
* Hex only, `string | null`. Delete a digit and the readout does not move:
* the model waits for a complete color, or for the field to be emptied.
*/
const brand = ref<string | null>('#4f46e5');
</script>
Do not parse the forwarded input event: it can contain partial text. The model is the validated channel.
Mobile picker
With adaptive (the default), only the picker becomes a bottom sheet below config.mobileMaxWidth; the text field does not change.
<template>
<!-- A settings column: label on the left, swatch and hex on the right. -->
<div class="flex max-w-md flex-col gap-2">
<BbColorInput
id="layout-primary"
v-model="primary"
compact
direction="xx xxxxxx"
label="Primary"
name="token-primary"
swatches
/>
<BbColorInput
id="layout-danger"
v-model="danger"
compact
direction="xx xxxxxx"
label="Danger"
name="token-danger"
swatches
/>
<BbColorInput
id="layout-warn"
v-model="warn"
compact
direction="xx xxxxxx"
label="Warning"
name="token-warn"
swatches
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbColorInput } from 'bitboss-ui';
const primary = ref<string | null>('#4f46e5');
const danger = ref<string | null>('#dc2626');
const warn = ref<string | null>('#f59e0b');
</script>
The surface is chosen when it opens. Use off-canvas-props for sheet controls and pt:sheet for sheet-only geometry.