Skip to content
BigBang

Popular

    ↑↓Navigate↵OpenEscClose
    TR

    Components

    FilterBar

    Filter controls for a list or table: applied-filter chips, a result count and one-click clear.

    Storybook
    Storybook
    Source
    Source

    The example below loads live from Storybook; open it in Storybook for the controls.

    Live example, StorybookOpen in Storybook

    FilterBar keeps the filters as one typed object. You define the controls with fields and manage the value with value and onValueChange. In the default instant mode every change applies at once. Applied filters show as chips, each with its own remove button. “Clear filters” appears only when a filter is applied.

    import { useState } from 'react';
    import { FilterBar, type FilterField } from '@bigbang/ui';
    type Filters = { query: string; status: string | null; category: string[]; mine: boolean };
    const empty: Filters = { query: '', status: null, category: [], mine: false };
    const fields: ReadonlyArray<FilterField<Filters>> = [
    { key: 'query', label: 'Search', kind: 'search', placeholder: 'Search by name' },
    {
    key: 'status',
    label: 'Status',
    kind: 'select',
    options: [
    { value: 'active', label: 'Active' },
    { value: 'draft', label: 'Draft' },
    { value: 'paused', label: 'Paused' },
    ],
    },
    {
    key: 'category',
    label: 'Category',
    kind: 'multi-select',
    options: [
    { value: 'a', label: 'Category A' },
    { value: 'b', label: 'Category B' },
    { value: 'c', label: 'Category C' },
    ],
    },
    { key: 'mine', label: 'Mine only', kind: 'toggle' },
    ];
    export default function Default() {
    const [value, setValue] = useState<Filters>({ ...empty, status: 'active', category: ['a'] });
    return (
    <div className="w-full">
    <FilterBar fields={fields} value={value} onValueChange={setValue} resultCount={12} />
    </div>
    );
    }

    With apply="batch" changes collect in a draft. The Apply button is enabled only when the draft differs from the applied value; Reset undoes the draft. Enter in the search box applies too. Chips always show the applied value.

    import { useState } from 'react';
    import { FilterBar, type FilterField } from '@bigbang/ui';
    type Filters = { query: string; status: string | null; category: string[]; mine: boolean };
    const empty: Filters = { query: '', status: null, category: [], mine: false };
    const fields: ReadonlyArray<FilterField<Filters>> = [
    { key: 'query', label: 'Search', kind: 'search', placeholder: 'Search by name' },
    {
    key: 'status',
    label: 'Status',
    kind: 'select',
    options: [
    { value: 'active', label: 'Active' },
    { value: 'draft', label: 'Draft' },
    { value: 'paused', label: 'Paused' },
    ],
    },
    {
    key: 'category',
    label: 'Category',
    kind: 'multi-select',
    options: [
    { value: 'a', label: 'Category A' },
    { value: 'b', label: 'Category B' },
    { value: 'c', label: 'Category C' },
    ],
    },
    { key: 'mine', label: 'Mine only', kind: 'toggle' },
    ];
    export default function Batch() {
    const [value, setValue] = useState<Filters>(empty);
    return (
    <div className="w-full">
    <FilterBar fields={fields} value={value} onValueChange={setValue} apply="batch" resultCount={12} />
    </div>
    );
    }

    FilterBar does not filter anything; it only produces the value. You filter the rows and give the number back with resultCount. The count is announced to screen readers once, 500 ms after the filters change.

    Item list
    NameStatusOwner
    First itemActiveUser 1
    Second itemDraftUser 2
    Third itemPausedUser 1
    Fourth itemActiveUser 2
    Fifth itemDraftUser 1
    Sixth itemActiveUser 2
    import { useState } from 'react';
    import { FilterBar, Table, type FilterField } from '@bigbang/ui';
    type Filters = { query: string; status: string | null; category: string[]; mine: boolean };
    const empty: Filters = { query: '', status: null, category: [], mine: false };
    const fields: ReadonlyArray<FilterField<Filters>> = [
    { key: 'query', label: 'Search', kind: 'search', placeholder: 'Search by name' },
    {
    key: 'status',
    label: 'Status',
    kind: 'select',
    options: [
    { value: 'active', label: 'Active' },
    { value: 'draft', label: 'Draft' },
    { value: 'paused', label: 'Paused' },
    ],
    },
    {
    key: 'category',
    label: 'Category',
    kind: 'multi-select',
    options: [
    { value: 'a', label: 'Category A' },
    { value: 'b', label: 'Category B' },
    { value: 'c', label: 'Category C' },
    ],
    },
    { key: 'mine', label: 'Mine only', kind: 'toggle' },
    ];
    const rows = [
    { name: 'First item', status: 'active', category: 'a', mine: true },
    { name: 'Second item', status: 'draft', category: 'b', mine: false },
    { name: 'Third item', status: 'paused', category: 'c', mine: true },
    { name: 'Fourth item', status: 'active', category: 'a', mine: false },
    { name: 'Fifth item', status: 'draft', category: 'b', mine: true },
    { name: 'Sixth item', status: 'active', category: 'c', mine: false },
    ];
    export default function WithTable() {
    const [value, setValue] = useState<Filters>(empty);
    const shown = rows.filter(
    (row) =>
    row.name.toLocaleLowerCase().includes(value.query.trim().toLocaleLowerCase()) &&
    (value.status === null || row.status === value.status) &&
    (value.category.length === 0 || value.category.includes(row.category)) &&
    (!value.mine || row.mine),
    );
    return (
    <div className="grid w-full gap-xl">
    <FilterBar fields={fields} value={value} onValueChange={setValue} resultCount={shown.length} />
    <Table caption="Item list" captionHidden>
    <Table.Header>
    <Table.Row>
    <Table.HeaderCell>Name</Table.HeaderCell>
    <Table.HeaderCell>Status</Table.HeaderCell>
    <Table.HeaderCell>Owner</Table.HeaderCell>
    </Table.Row>
    </Table.Header>
    <Table.Body>
    {shown.map((row) => (
    <Table.Row key={row.name}>
    <Table.RowHeader>{row.name}</Table.RowHeader>
    <Table.Cell>{fields[1]?.options?.find((option) => option.value === row.status)?.label}</Table.Cell>
    <Table.Cell>{row.mine ? 'User 1' : 'User 2'}</Table.Cell>
    </Table.Row>
    ))}
    {shown.length === 0 ? (
    <Table.Row>
    <Table.Cell colSpan={3}>No item matches the filters</Table.Cell>
    </Table.Row>
    ) : null}
    </Table.Body>
    </Table>
    </div>
    );
    }

    Below 1024 px the search box stays in place; the other filters move into an inline panel opened by the “More filters” button. The button shows the number of applied filters in a badge. A drawer for very narrow screens is not built yet.

    FilterBar takes every colour from tokens, and the controls inside follow their own modes.

    import { useState } from 'react';
    import { FilterBar, type FilterField } from '@bigbang/ui';
    type Filters = { query: string; status: string | null; category: string[]; mine: boolean };
    const empty: Filters = { query: '', status: null, category: [], mine: false };
    const fields: ReadonlyArray<FilterField<Filters>> = [
    { key: 'query', label: 'Search', kind: 'search', placeholder: 'Search by name' },
    {
    key: 'status',
    label: 'Status',
    kind: 'select',
    options: [
    { value: 'active', label: 'Active' },
    { value: 'draft', label: 'Draft' },
    { value: 'paused', label: 'Paused' },
    ],
    },
    {
    key: 'category',
    label: 'Category',
    kind: 'multi-select',
    options: [
    { value: 'a', label: 'Category A' },
    { value: 'b', label: 'Category B' },
    { value: 'c', label: 'Category C' },
    ],
    },
    { key: 'mine', label: 'Mine only', kind: 'toggle' },
    ];
    export default function Default() {
    const [value, setValue] = useState<Filters>({ ...empty, status: 'active', category: ['a'] });
    return (
    <div className="w-full">
    <FilterBar fields={fields} value={value} onValueChange={setValue} resultCount={12} />
    </div>
    );
    }