Filter
This component enables the creation of nested filter menus with support for state management, navigation, and optional reset functionality. It uses height transitions for smooth visual changes and dynamically organizes filter content based on provided slots, making it adaptable to varying needs.
TIP
Don't make your view too complex. Limit yourself to one filter per view.
Required icons
Props
model-value: FluxFilterState
The filter state.
Emits
update:model-value: [FluxFilterState]
Triggered when the filter state changes.
clear: [string]
Triggered when a filter's value is cleared via the trash button. Receives the name of the cleared filter.
reset: [string]
Triggered when a filter is reset to its default value. Receives the name of the reset filter.
Slots
default
This slot should contain filters or a separator.
TIP
Looking for a toolbar-style filter with a search input? See Filter bar.
Available filters
Common props
Every filter component (built-in and custom) accepts the following props in addition to its own:
default-value?: FluxFilterValue
Initial value applied when state is undefined. Reset returns to this value.
disabled?: boolean
Filter is shown but not interactive.
on-change?: (value: FluxFilterValue) => void
Called after the filter's value mutates.
on-clear?: () => void
Called when the filter is reset.
Custom filter types
Build your own filter component by calling defineFilter() on the top level of <script setup>. The macro registers a factory that FluxFilter and FluxFilterBar invoke to obtain the filter's runtime metadata (label, icon, badge text, lifecycle, …). The component name does not matter — any component that calls defineFilter() is accepted.
Vite plugin required
defineFilter() is a compile-time macro. Add the plugin from @flux-ui/components/vite to your Vite config — without it the call is left as a no-op and the filter will not register.
import { defineFilterMacro } from '@flux-ui/components/vite';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
defineFilterMacro(),
vue()
]
});Custom toggle filter
Defining a boolean toggle filter using `defineFilter`.
<template>
<Preview>
<FluxPane style="width: max-content; align-self: start">
<FluxFilter
v-model="filterState">
<MyToggleFilter
icon="bell"
label="Notifications"
name="notifications"
:default-value="true"/>
<MyToggleFilter
icon="moon"
label="Dark mode"
name="darkMode"/>
</FluxFilter>
</FluxPane>
</Preview>
</template>
<script
lang="ts"
setup>
import { FluxFilter, FluxPane } from '@flux-ui/components';
import type { FluxFilterState } from '@flux-ui/types';
import { ref } from 'vue';
import MyToggleFilter from './MyToggleFilter.vue';
const filterState = ref<FluxFilterState>({});
</script>The toggle component above is implemented like this:
<template>
<FluxMenuGroup>
<FluxMenuItem
is-selectable
:is-selected="state[name] === true"
label="Enabled"
@click="onSelect(true)"/>
<FluxMenuItem
is-selectable
:is-selected="state[name] === false"
label="Disabled"
@click="onSelect(false)"/>
</FluxMenuGroup>
</template>
<script
lang="ts"
setup>
import { defineFilter, FluxMenuGroup, FluxMenuItem, pickFilterCommon, useFilterInjection } from '@flux-ui/components';
import type { FluxFilterSpec } from '@flux-ui/types';
type Props = FluxFilterSpec;
defineFilter<Props>(p => ({
...pickFilterCommon(p),
type: 'toggle',
async getValueLabel(value) {
if (value === true) {
return 'Enabled';
}
if (value === false) {
return 'Disabled';
}
return null;
}
}));
const {
name
} = defineProps<Props>();
const {back, state, setValue} = useFilterInjection();
function onSelect(value: boolean): void {
setValue(name, value);
back();
}
</script><template>
<Preview>
<FluxPane style="width: max-content; align-self: start">
<FluxFilter
v-model="filterState">
<MyToggleFilter
icon="bell"
label="Notifications"
name="notifications"
:default-value="true"/>
<MyToggleFilter
icon="moon"
label="Dark mode"
name="darkMode"/>
</FluxFilter>
</FluxPane>
</Preview>
</template>
<script
lang="ts"
setup>
import { FluxFilter, FluxPane } from '@flux-ui/components';
import type { FluxFilterState } from '@flux-ui/types';
import { ref } from 'vue';
import MyToggleFilter from './MyToggleFilter.vue';
const filterState = ref<FluxFilterState>({});
</script>Examples
Basic
A basic example of the filter.
<template>
<FluxPane style="width: max-content; align-self: start">
<FluxFilter v-model="filterState">
<FluxFilterOption
is-searchable
icon="clone"
label="Option"
name="option1"
search-placeholder="Search options..."
:options="[
{label: 'Option A', value: 'a'},
{label: 'Option B', value: 'b'},
{label: 'Option C', value: 'c'}
]"/>
<FluxFilterOptions
is-searchable
icon="circle-check"
label="Choices"
name="option2"
search-placeholder="Search options..."
:options="[
{label: 'Option A', value: 'a'},
{label: 'Option B', value: 'b'},
{label: 'Option C', value: 'c'}
]"/>
<FluxSeparator/>
<FluxFilterOptionAsync
icon="hourglass-clock"
label="Option (Async)"
name="option6"
search-placeholder="Search async options..."
:fetch-options="fetchOptions"
:fetch-relevant="fetchRelevant"
:fetch-search="fetchSearch"/>
<FluxFilterOptionsAsync
icon="hourglass-clock"
label="Choices (Async)"
name="option7"
search-placeholder="Search async options..."
:fetch-options="fetchOptions"
:fetch-relevant="fetchRelevant"
:fetch-search="fetchSearch"/>
<FluxSeparator/>
<FluxFilterDate
icon="calendar"
label="Date"
name="option3"/>
<FluxFilterDateRange
icon="calendar-range"
label="Period"
name="option4"/>
<FluxSeparator/>
<FluxFilterRange
icon="coin"
label="Cost"
name="option5"
:formatter="rangeFormatter"
:max="1000"
:min="0"
:step="10"/>
</FluxFilter>
</FluxPane>
</template>
<script
lang="ts"
setup>
import { FluxFilter, FluxFilterDate, FluxFilterDateRange, FluxFilterOption, FluxFilterOptionAsync, FluxFilterOptions, FluxFilterOptionsAsync, FluxFilterRange, FluxPane, FluxSeparator } from '@flux-ui/components';
import type { FluxFilterOptionRow, FluxFilterState } from '@flux-ui/types';
import { DateTime } from 'luxon';
import { ref } from 'vue';
import dataset from '../../../assets/select-dataset.json' with { type: 'json' };
async function fetchOptions(values: string[]): Promise<FluxFilterOptionRow[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return dataset.filter(o => values.includes(o.value));
}
async function fetchRelevant(): Promise<FluxFilterOptionRow[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return dataset.toSorted();
}
async function fetchSearch(searchQuery: string): Promise<FluxFilterOptionRow[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return dataset.filter(o => o.label.toLowerCase().includes(searchQuery.toLowerCase()));
}
const filterState = ref<FluxFilterState>({
option1: 'b',
option2: ['a', 'c'],
option3: DateTime.now(),
option4: [DateTime.now(), DateTime.now().plus({day: 14})],
option5: [250, 500],
option6: '73c83353-de92-8110-9bce-c2a9e8c0de64',
option7: ['73c83353-de92-8110-9bce-c2a9e8c0de64', '92f99357-7fe5-71eb-74e2-55e057607e16']
});
function rangeFormatter(value: number): string {
const formatter = new Intl.NumberFormat(navigator.language, {
currency: 'EUR',
maximumFractionDigits: 2,
minimumFractionDigits: 2,
style: 'currency'
});
return formatter.format(value / 100);
}
</script>Flyout
A filter that pops up when you press on a button.
<template>
<FluxFlyout>
<template #opener="{ open }">
<FluxSecondaryButton
icon-leading="filter" label="Filter items"
@click="open"/>
</template>
<FluxPane style="width: max-content; align-self: start">
<FluxFilter v-model="filterState">
<FluxFilterOption
is-searchable
icon="clone"
label="Option"
name="option1"
search-placeholder="Search options..."
:options="[
{label: 'Option A', value: 'a'},
{label: 'Option B', value: 'b'},
{label: 'Option C', value: 'c'}
]"/>
<FluxFilterOptions
is-searchable
icon="circle-check"
label="Choices"
name="option2"
search-placeholder="Search options..."
:options="[
{label: 'Option A', value: 'a'},
{label: 'Option B', value: 'b'},
{label: 'Option C', value: 'c'}
]"/>
<FluxSeparator/>
<FluxFilterOptionAsync
icon="hourglass-clock"
label="Option (Async)"
name="option6"
search-placeholder="Search async options..."
:fetch-options="fetchOptions"
:fetch-relevant="fetchRelevant"
:fetch-search="fetchSearch"/>
<FluxFilterOptionsAsync
icon="hourglass-clock"
label="Choices (Async)"
name="option7"
search-placeholder="Search async options..."
:fetch-options="fetchOptions"
:fetch-relevant="fetchRelevant"
:fetch-search="fetchSearch"/>
<FluxSeparator/>
<FluxFilterDate
icon="calendar"
label="Date"
name="option3"/>
<FluxFilterDateRange
icon="calendar-range"
label="Period"
name="option4"/>
<FluxSeparator/>
<FluxFilterRange
icon="coin"
label="Cost"
name="option5"
:formatter="rangeFormatter"
:max="1000"
:min="0"
:step="10"/>
</FluxFilter>
</FluxPane>
</FluxFlyout>
</template>
<script
lang="ts"
setup>
import { FluxFilter, FluxFilterDate, FluxFilterDateRange, FluxFilterOption, FluxFilterOptionAsync, FluxFilterOptions, FluxFilterOptionsAsync, FluxFilterRange, FluxFlyout, FluxPane, FluxSecondaryButton, FluxSeparator } from '@flux-ui/components';
import type { FluxFilterOptionRow, FluxFilterState } from '@flux-ui/types';
import { DateTime } from 'luxon';
import { ref } from 'vue';
import dataset from '../../../assets/select-dataset.json' with { type: 'json' };
async function fetchOptions(values: string[]): Promise<FluxFilterOptionRow[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return dataset.filter(o => values.includes(o.value));
}
async function fetchRelevant(): Promise<FluxFilterOptionRow[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return dataset.toSorted();
}
async function fetchSearch(searchQuery: string): Promise<FluxFilterOptionRow[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return dataset.filter(o => o.label.toLowerCase().includes(searchQuery.toLowerCase()));
}
const filterState = ref<FluxFilterState>({
option1: 'b',
option2: ['a', 'c'],
option3: DateTime.now(),
option4: [DateTime.now(), DateTime.now().plus({day: 14})],
option5: [250, 500],
option6: '73c83353-de92-8110-9bce-c2a9e8c0de64',
option7: ['73c83353-de92-8110-9bce-c2a9e8c0de64', '92f99357-7fe5-71eb-74e2-55e057607e16']
});
function rangeFormatter(value: number): string {
const formatter = new Intl.NumberFormat(navigator.language, {
currency: 'EUR',
maximumFractionDigits: 2,
minimumFractionDigits: 2,
style: 'currency'
});
return formatter.format(value / 100);
}
</script>