Appearance
Selectable Chip
The FTSelectableChip component is a blue selectable chip used for origins, markets, and features. It replaces the legacy gray origin selector (DEV-13469). Each chip owns its own selection state via v-model, making it easy to compose multi-select lists where the parent array drives the UI.
Basic usage
vue
<script setup lang="ts">
import { ref } from 'vue';
import { FTSelectableChip } from '@fasttrack-solutions/vue-components-lib';
import type { IChipItem } from '@fasttrack-solutions/vue-components-lib';
const item = ref<IChipItem>({ id: 1, name: 'Malta', isSelected: false });
</script>
<template>
<FTSelectableChip v-model="item" @item-clicked="console.log('toggled', item.isSelected)" />
</template><script setup lang="ts">
import { ref } from 'vue';
import { FTSelectableChip } from '@fasttrack-solutions/vue-components-lib';
import type { IChipItem } from '@fasttrack-solutions/vue-components-lib';
const item = ref<IChipItem>({ id: 1, name: 'Malta', isSelected: false });
</script>
<template>
<FTSelectableChip v-model="item" @item-clicked="console.log('toggled', item.isSelected)" />
</template>Playground
Origin name
Properties
<script setup lang="ts">
import { ref } from 'vue';
const item = ref({
"id": 1,
"name": "Origin name",
"isSelected": true,
"isDisabled": false
});
</script>
<template>
<FTSelectableChip v-model="item" />
</template>
IChipItem model shape
The component is driven entirely by the v-model object. All fields are optional.
| Field | Type | Description |
|---|---|---|
id | number | Optional identifier — passed through in events but not rendered |
name | string | Chip label text |
icon | string | FontAwesome icon string (e.g. "fas user"). Renders an FTIcon before the label. Takes precedence over logoUrl — if both are set, only the icon is shown. |
logoUrl | string | URL for a 24 × 24 circular logo or flag image. Flags are just a CDN image URL (e.g. ${cdnBase}/flags/se.svg) — no separate flag prop is needed. |
count | number | Trailing numeric count (localized). Rendered when >= 0 |
isSelected | boolean | Controls the selected visual state and aria-pressed |
isDisabled | boolean | Prevents interaction and mutes the chip visually |
tooltip | string | Balloon tooltip text shown on hover (suppressed when readOnly is true) |
Multi-select list
The component owns its own toggle via v-model. The parent's @item-clicked handler is read-only — use it to react (e.g. log or sync to a server) without flipping isSelected again, which would double-toggle.
vue
<script setup lang="ts">
import { ref, computed } from 'vue';
import { FTSelectableChip } from '@fasttrack-solutions/vue-components-lib';
import type { IChipItem } from '@fasttrack-solutions/vue-components-lib';
const origins = ref<IChipItem[]>([
{ id: 1, name: 'Malta', logoUrl: '/flags/mt.svg', isSelected: false },
{ id: 2, name: 'Sweden', logoUrl: '/flags/se.svg', isSelected: true },
{ id: 3, name: 'Germany', logoUrl: '/flags/de.svg', isSelected: false },
]);
const selectedNames = computed(() =>
origins.value.filter((o) => o.isSelected).map((o) => o.name),
);
function onItemClicked(index: number) {
// Read-only: the chip already updated origins[index].isSelected via v-model.
// React here (e.g. analytics, server sync) — do NOT flip isSelected again.
console.log('selection changed:', origins.value[index].name, origins.value[index].isSelected);
}
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<FTSelectableChip
v-for="(origin, i) in origins"
:key="origin.id"
v-model="origins[i]"
@item-clicked="onItemClicked(i)"
/>
</div>
<p>Selected: {{ selectedNames.join(', ') || 'none' }}</p>
</template><script setup lang="ts">
import { ref, computed } from 'vue';
import { FTSelectableChip } from '@fasttrack-solutions/vue-components-lib';
import type { IChipItem } from '@fasttrack-solutions/vue-components-lib';
const origins = ref<IChipItem[]>([
{ id: 1, name: 'Malta', logoUrl: '/flags/mt.svg', isSelected: false },
{ id: 2, name: 'Sweden', logoUrl: '/flags/se.svg', isSelected: true },
{ id: 3, name: 'Germany', logoUrl: '/flags/de.svg', isSelected: false },
]);
const selectedNames = computed(() =>
origins.value.filter((o) => o.isSelected).map((o) => o.name),
);
function onItemClicked(index: number) {
// Read-only: the chip already updated origins[index].isSelected via v-model.
// React here (e.g. analytics, server sync) — do NOT flip isSelected again.
console.log('selection changed:', origins.value[index].name, origins.value[index].isSelected);
}
</script>
<template>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<FTSelectableChip
v-for="(origin, i) in origins"
:key="origin.id"
v-model="origins[i]"
@item-clicked="onItemClicked(i)"
/>
</div>
<p>Selected: {{ selectedNames.join(', ') || 'none' }}</p>
</template>States
| State | Trigger | Visual |
|---|---|---|
| Default | isSelected: false | Mono-100 background, mono-300 border |
| Hover | Mouse over (default state) | Mono-200 background, mono-400 border |
| Selected | isSelected: true | Blue-100 background, blue-500 border, bold label |
| Selected hover | Mouse over (selected state) | Blue-200 background |
| Disabled | isDisabled: true or readOnly: true | Default colors at 40 % opacity, pointer events blocked |
| Closable | closable: true | Trailing ×-mark button; click emits itemClosed (removal signal; parent removes the item from its array). The ✕ is hidden when the chip is disabled or read-only. |
| With count | count field >= 0 | Trailing localized number next to the label |
| With logo | logoUrl set | 24 × 24 circular image before the label; falls back to flagFallbackUrl on error |
| With icon | icon set | FTIcon before the label at 20 px; takes precedence over logoUrl |
Props
| Name | Default | Description |
|---|---|---|
modelValue | required | IChipItem — the chip's data and state object (use v-model) |
readOnly | false | Blocks all interaction and suppresses tooltips; chip appears visually disabled |
closable | false | Shows a trailing ×-mark button. Clicking it emits itemClosed — the parent should remove the item from its array in response |
flagFallbackUrl | '' | Fallback image URL shown when logoUrl fails to load (e.g. a generic flag placeholder) |
Events
| Name | Payload | Description |
|---|---|---|
itemClicked | — | Emitted after the chip's selection state is toggled (not emitted when disabled) |
itemClosed | — | Emitted when the ×-mark close button is clicked (requires closable: true) |
update:modelValue | IChipItem | Updated chip object after toggle (emitted automatically by v-model/defineModel) |
CSS custom properties
| Variable | Default | Description |
|---|---|---|
--ft-selectable-chip-bg | var(--color-mono-100) | Background colour in the default state |
--ft-selectable-chip-border | var(--color-mono-300) | Border colour in the default state |
--ft-selectable-chip-fg | var(--color-mono-700) | Text and icon colour |
--ft-selectable-chip-hover-bg | var(--color-mono-200) | Background on hover (unselected) |
--ft-selectable-chip-hover-border | var(--color-mono-400) | Border on hover (unselected) |
--ft-selectable-chip-selected-bg | var(--color-brand-blue-100) | Background when selected |
--ft-selectable-chip-selected-border | var(--color-brand-blue-500) | Border when selected |
--ft-selectable-chip-hover-selected-bg | var(--color-brand-blue-200) | Background on hover when selected |
Token caveat for older lib versions
--ft-selectable-chip-selected-bg defaults to var(--color-brand-blue-100) (#d8edf9). Apps running an older version of the library (e.g. 4.2.0) where --color-brand-blue-100 is not defined will see no selected background. Override the custom property at your app's root or the chip's parent to resolve:
css
/* In your app's global stylesheet or scoped to the chip's container */
--ft-selectable-chip-selected-bg: var(--color-brand-blue-200);/* In your app's global stylesheet or scoped to the chip's container */
--ft-selectable-chip-selected-bg: var(--color-brand-blue-200);