Chip
A compact, styled label for tags, filters, and selected values. Supports a leading icon, interaction modes (static, removable, selectable), all color themes, five variants, and three sizes. Used internally by bit-select and bit-combobox in multiselect mode.
Features
- 🎨 5 Variants: solid, flat, bordered, outline, ghost
- 🌈 6 Semantic Colors: primary, secondary, info, success, warning, error
- 📏 3 Sizes: sm, md, lg
- ❌ Removable: optional × button that fires
remove - ✅ Selectable: toggle chip state with
mode="selectable"and thechangeevent - ⚡ Action: stateless button-like chip that fires a
clickevent - 🖼️ Icon Slot: prepend an icon or decoration
- ♿ Accessible: remove button has a contextual
aria-labelincluding the chip value
Source Code
View Source Code
import { define, computed, html, signal, watch } from '@vielzeug/craftit';
import type { ComponentSize, RoundedSize, ThemeColor, VisualVariant } from '../../types';
import '../../content/icon/icon';
import {
colorThemeMixin,
disabledStateMixin,
forcedColorsMixin,
roundedVariantMixin,
sizeVariantMixin,
} from '../../styles';
// ============================================
// Styles
// ============================================
import componentStyles from './chip.css?inline';
// ============================================
// Types
// ============================================
/** Chip component properties */
type ChipBaseProps = {
/** Theme color */
color?: ThemeColor;
/** Disable interactions */
disabled?: boolean;
/** Accessible label (required for icon-only chips) */
label?: string;
/** Border radius override */
rounded?: RoundedSize | '';
/** Component size */
size?: ComponentSize;
/** Value associated with this chip — included in emitted event detail */
value?: string;
/** Visual style variant */
variant?: Exclude<VisualVariant, 'glass' | 'text' | 'frost'>;
};
type BitChipMode = 'static' | 'removable' | 'selectable' | 'action';
/** Read-only presentation chip */
type StaticChipProps = {
mode?: Extract<BitChipMode, 'static'>;
};
/** Removable chip mode */
type RemovableChipProps = {
mode: Extract<BitChipMode, 'removable'>;
};
/** Selectable chip mode */
type SelectableChipProps = {
/** Controlled checked state for `mode="selectable"` */
checked?: boolean | undefined;
/** Initial checked state for uncontrolled `mode="selectable"` */
'default-checked'?: boolean;
mode: Extract<BitChipMode, 'selectable'>;
};
/** Action chip mode — behaves like a button, fires a click event without maintaining state */
type ActionChipProps = {
mode: Extract<BitChipMode, 'action'>;
};
type BitChipComponentProps = ChipBaseProps & {
checked?: boolean | undefined;
'default-checked'?: boolean;
mode?: BitChipMode;
};
export type BitChipEvents = {
change: { checked: boolean; originalEvent: Event; value: string | undefined };
click: { originalEvent: MouseEvent; value: string | undefined };
remove: { originalEvent: MouseEvent; value: string | undefined };
};
export type BitChipProps = ChipBaseProps &
(StaticChipProps | RemovableChipProps | SelectableChipProps | ActionChipProps);
/**
* A compact, styled label element. Supports icons, a remove button, colors, sizes, and variants.
* Commonly used to represent tags, filters, or selected options in a multiselect field.
*
* @element bit-chip
*
* @attr {string} label - Accessible label (required for icon-only chips)
* @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'
* @attr {string} variant - Visual variant: 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost'
* @attr {string} size - Component size: 'sm' | 'md' | 'lg'
* @attr {string} rounded - Border radius: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'
* @attr {string} mode - Interaction mode: 'static' | 'removable' | 'selectable' | 'action'
* @attr {boolean} disabled - Disable the chip
* @attr {string} value - Value included in emitted event detail
* @attr {boolean} checked - Controlled checked state for selectable chips
* @attr {boolean} default-checked - Initial checked state for uncontrolled selectable chips
*
* @slot - Chip label text
* @slot icon - Leading icon or decoration
*
* @event remove - Fired when the remove button is clicked, with `detail.value` and `detail.originalEvent`
* @event change - Fired when a selectable chip toggles, with `detail.checked`, `detail.value`, and `detail.originalEvent`
* @event click - Fired when an action chip is clicked, with `detail.value` and `detail.originalEvent`
*
* @cssprop --chip-bg - Background color
* @cssprop --chip-color - Text color
* @cssprop --chip-border-color - Border color
* @cssprop --chip-radius - Border radius
* @cssprop --chip-font-size - Font size
* @cssprop --chip-font-weight - Font weight
* @cssprop --chip-padding-x - Horizontal padding
* @cssprop --chip-padding-y - Vertical padding
* @cssprop --chip-gap - Gap between icon, label and remove button
*
* @example
* ```html
* <!-- Static chip (read-only) -->
* <bit-chip color="primary">Design</bit-chip>
*
* <!-- Removable chip -->
* <bit-chip color="success" variant="flat" mode="removable" value="ts">
* TypeScript
* </bit-chip>
*
* <!-- Selectable chip (controlled) -->
* <bit-chip color="info" variant="flat" mode="selectable" checked value="ui">
* UI
* </bit-chip>
*
* <!-- Selectable chip (uncontrolled) -->
* <bit-chip color="info" variant="flat" mode="selectable" default-checked value="ui">
* UI
* </bit-chip>
*
* <!-- Action chip (acts like a button) -->
* <bit-chip color="warning" mode="action" value="add">
* <svg slot="icon" ...></svg>
* Add Item
* </bit-chip>
*
* <!-- Icon-only chip -->
* <bit-chip color="error" mode="action" label="Delete">
* <svg slot="icon" ...></svg>
* </bit-chip>
* ```
*/
export const CHIP_TAG = define<BitChipComponentProps, BitChipEvents>('bit-chip', {
props: {
checked: {
default: undefined as BitChipComponentProps['checked'],
parse: (value: string | null) => (value == null ? undefined : value !== 'false'),
},
color: undefined,
'default-checked': false,
disabled: false,
label: undefined,
mode: 'static',
rounded: undefined,
size: undefined,
value: undefined,
variant: undefined,
},
setup({ emit, host, props }) {
const checkedProp = props.checked;
const labelProp = props.label;
// ============================================
// State Management
// ============================================
// Once a checked prop is provided, treat the chip as controlled for the rest of its lifecycle.
const isControlled = signal(checkedProp.value !== undefined);
// Internal tracking for uncontrolled selectable chips; seeded from default-checked.
const checkedState = signal(!isControlled.value && props['default-checked'].value);
watch(checkedProp, (value) => {
if (value !== undefined) {
isControlled.value = true;
}
});
// Effective checked value — reactive to checked prop changes in controlled mode.
const isChecked = computed(() => {
if (props.mode.value !== 'selectable') return false;
if (isControlled.value) {
return checkedProp.value ?? false;
}
return checkedState.value;
});
host.bind('attr', {
checked: () => (props.mode.value === 'selectable' && isChecked.value ? true : undefined),
});
// ============================================
// Event Handlers
// ============================================
function handleRemove(e: MouseEvent) {
if (props.disabled.value) return;
emit('remove', { originalEvent: e, value: props.value.value });
}
function handleSelectableActivate(e: MouseEvent) {
e.stopPropagation();
if (props.disabled.value) return;
const nextChecked = !isChecked.value;
if (!isControlled.value) {
checkedState.value = nextChecked;
}
emit('change', { checked: nextChecked, originalEvent: e, value: props.value.value });
}
function handleActionClick(e: MouseEvent) {
if (props.disabled.value) return;
emit('click', { originalEvent: e, value: props.value.value });
}
// ============================================
// Template Helpers
// ============================================
const renderChipContent = () => html`
<slot name="icon"></slot>
<span class="label"><slot></slot></span>
`;
const renderRemoveButton = () => html`
<button
class="remove-btn"
part="remove-btn"
type="button"
:aria-label="${() => {
const label = labelProp.value || props.value.value;
return label ? `Remove ${label}` : 'Remove';
}}"
?hidden="${() => props.mode.value !== 'removable'}"
:disabled="${() => props.disabled.value}"
@click="${handleRemove}">
<bit-icon name="x" size="12" stroke-width="2.5" aria-hidden="true"></bit-icon>
</button>
`;
const renderSelectableChip = () => html`
<button
class="chip-btn"
part="chip-btn"
type="button"
role="checkbox"
:aria-checked="${() => String(isChecked.value)}"
:aria-label="${() => labelProp.value}"
:disabled="${() => props.disabled.value}"
@click="${handleSelectableActivate}">
<span class="chip" part="chip"> ${renderChipContent()} </span>
</button>
`;
const renderActionChip = () => html`
<button
class="chip-btn"
part="chip-btn"
type="button"
:aria-label="${() => labelProp.value}"
:disabled="${() => props.disabled.value}"
@click="${handleActionClick}">
<span class="chip" part="chip"> ${renderChipContent()} </span>
</button>
`;
const renderStaticChip = () => html` <span class="chip" part="chip"> ${renderChipContent()} </span> `;
const renderRemovableChip = () => html`
<span class="chip" part="chip"> ${renderChipContent()} ${renderRemoveButton()} </span>
`;
// ============================================
// Render
// ============================================
return html`
${() => {
const mode = props.mode.value;
if (mode === 'selectable') return renderSelectableChip();
if (mode === 'action') return renderActionChip();
if (mode === 'removable') return renderRemovableChip();
return renderStaticChip();
}}
`;
},
styles: [
colorThemeMixin,
disabledStateMixin(),
roundedVariantMixin,
sizeVariantMixin({
lg: {
'--_font-size': 'var(--text-sm)',
'--_gap': 'var(--size-1-5)',
'--_padding-x': 'var(--size-3)',
'--_padding-y': 'var(--size-1)',
},
sm: {
'--_font-size': 'var(--text-xs)',
'--_gap': 'var(--size-0-5)',
'--_padding-x': 'var(--size-2-5)',
'--_padding-y': 'var(--size-px)',
},
}),
forcedColorsMixin,
componentStyles,
],
});Basic Usage
Variants
Five visual variants for different levels of emphasis.
Colors
Sizes
Removable
Set mode="removable" to show a × button. Listen for remove to handle removal. The value attribute is included in the event detail.
document.querySelectorAll('bit-chip').forEach((chip) => {
chip.addEventListener('remove', (e) => {
console.log('removed:', e.detail.value, 'source event:', e.detail.originalEvent?.type);
e.target.remove();
});
});Selectable
Set mode="selectable" to make the chip behave like a checkbox-like toggle. Use change to react to state updates.
document.querySelectorAll('bit-chip[mode="selectable"]').forEach((chip) => {
chip.addEventListener('change', (e) => {
console.log('checked:', e.detail.checked, 'value:', e.detail.value, 'source:', e.detail.originalEvent?.type);
});
});Action
Set mode="action" to make the chip behave like a button — it fires a click event but holds no internal state. Use it for quick actions, command triggers, or suggestion pills.
document.querySelectorAll('bit-chip[mode="action"]').forEach((chip) => {
chip.addEventListener('click', (e) => {
console.log('action:', e.detail.value, 'source:', e.detail.originalEvent?.type);
});
});With Icon
Use the icon named slot to prepend a leading icon or glyph.
Rounded
Override the border radius with the rounded attribute.
Disabled
Building a Tag Input
Chips are designed to compose with form controls. Here is a minimal tag-input pattern built on top of bit-chip:
document.getElementById('tag-wrap').addEventListener('remove', (e) => {
e.target.remove();
console.log('tag removed:', e.detail.value);
});API Reference
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | Accessible label for icon-only chips and custom action text |
color | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error' | — | Color theme |
variant | 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'solid' | Visual style variant |
size | 'sm' | 'md' | 'lg' | 'md' | Chip size |
rounded | 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' | — | Border radius override |
mode | 'static' | 'removable' | 'selectable' | 'action' | 'static' | Interaction mode |
disabled | boolean | false | Disable the chip (remove button becomes non-functional) |
value | string | — | Value passed in the remove, change, and click event detail |
checked | boolean | — | Controlled checked state for selectable chips |
default-checked | boolean | false | Initial checked state in uncontrolled selectable mode |
Slots
| Slot | Description |
|---|---|
| (default) | Chip label text |
icon | Leading icon or decoration |
Events
| Event | Detail | Description |
|---|---|---|
remove | { value: string | undefined, originalEvent: MouseEvent } | Fired when the remove button is clicked |
change | { value: string | undefined, checked: boolean, originalEvent: MouseEvent } | Fired when a selectable chip toggles |
click | { value: string | undefined, originalEvent: MouseEvent } | Fired when an action chip is clicked |
CSS Custom Properties
| Property | Description | Default |
|---|---|---|
--chip-bg | Background color | Variant-dependent |
--chip-color | Text color | Variant-dependent |
--chip-border-color | Border color | Variant-dependent |
--chip-radius | Border radius | --rounded-full |
--chip-font-size | Font size | --text-sm |
--chip-font-weight | Font weight | --font-medium |
--chip-padding-x | Horizontal padding | --size-2-5 |
--chip-padding-y | Vertical padding | --size-0-5 |
--chip-gap | Gap between icon, label, and remove button | --size-1 |
Accessibility
The chip component follows WAI-ARIA best practices.
bit-chip
✅ Keyboard Navigation
- The remove button is keyboard-accessible.
- When
disabled, the remove button has thedisabledattribute preventing activation.
✅ Screen Readers
- The remove button has a contextual
aria-label:"Remove {value}"whenvalueis set,"Remove"otherwise. - Selectable chips use
role="checkbox"andaria-checkedwhile preserving the visible label as the accessible name. - Action chips render as a
<button>element; supplyaria-labelfor icon-only chips. - Use
valueto identify which chip triggered an event in a list.