Skip to content

Filter

The filter builds nested filter menus with state management, navigation, and optional reset support. It organizes its content from the slots you provide and animates height changes as users move between views.

TIP

Don't make your view too complex. Limit yourself to one filter per view.

Required icons

angle-left
angle-right
rotate-left
trash

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. Also called by reset when a default-value is set, receiving that default value.

on-clear?: () => void
Called when the filter is cleared, and when reset has no default-value to fall back to.

Clear vs. reset

Clearing a filter removes its key from the state entirely and calls on-clear. Resetting a filter that has a default-value writes that value back and calls on-change; resetting a filter without a default-value behaves like clear and removes the key while calling on-clear.

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.

vite.config.ts
ts
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>
    <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>
</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:

vue
<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>
vue
<template>
    <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>
</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>

Used components