Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .storybook/components/Roadmap/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,12 @@ export const rows: Rows = [
stage: '🔵 experimental',
planned: 'Q3 2026',
},
{
component: 'Highlight',
status: '✅ Done',
stage: '🔵 experimental',
planned: 'Q3 2026',
},
{
component: 'DropdownMenu',
status: '✅ Done',
Expand Down Expand Up @@ -443,9 +449,4 @@ export const rows: Rows = [
status: '🚧 Planned',
planned: 'Q3 2026',
},
{
component: 'Highlight',
status: '🚧 Planned',
planned: 'Q3 2026',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ The following example uses the `defaultFilter` prop to filter the list of option

<Story of={Stories.CustomFiltering} />

### Highlighting matches

Use [Highlight](/docs/components-highlight--docs) to mark the part of an option that matched the query.
Control `inputValue` so the query is available for both filtering and highlighting, and set `textValue`
on the item so the collection keeps a plain-text label for typeahead and accessibility.

<Story of={Stories.HighlightingMatches} />

## Appearance

### Addons
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Meta, StoryObj } from '@storybook/react';

import { Badge, useFilter } from '../../index';
import { FlexBox } from '../FlexBox';
import { Highlight } from '../Highlight';
import { ProgressSpinner } from '../ProgressSpinner';
import { Typography } from '../Typography';

Expand Down Expand Up @@ -273,6 +274,40 @@ export const CustomFiltering: Story = {
},
};

export const HighlightingMatches: Story = {
render: function Render() {
const items = [
{ key: 'tls', name: 'TLS' },
{ key: 'ssh', name: 'SSH' },
{ key: 'pgp', name: 'PGP' },
{ key: 'ipsec', name: 'IPSec' },
{ key: 'kerberos', name: 'Kerberos' },
];

const { contains } = useFilter({ sensitivity: 'base' });

const [inputValue, setInputValue] = useState('');

const filtered = items.filter((item) => contains(item.name, inputValue));

return (
<Autocomplete
label="Protocol"
items={filtered}
placeholder="Search a protocol"
inputValue={inputValue}
onInputChange={setInputValue}
>
{(item) => (
<Autocomplete.Item key={item.key} textValue={item.name}>
<Highlight text={item.name} query={inputValue} />
</Autocomplete.Item>
)}
</Autocomplete>
);
},
};

export const Addons: Story = {
render: function Render(args) {
return (
Expand Down
96 changes: 96 additions & 0 deletions packages/components/src/components/Highlight/Highlight.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
Meta,
Story,
Props,
Status,
} from '../../../../../.storybook/components';

import * as Stories from './Highlight.stories';

<Meta of={Stories} />

# Highlight

<Status variant="experimental" />

Highlight marks every occurrence of a search query inside a text.
Marking matches helps users understand why a result is relevant and speeds up
scanning through long lists.

Matching is case-insensitive, covers all occurrences, and treats the query
literally — regular expression characters such as `.` or `[0]` need no escaping.
Matched fragments are wrapped in a `<mark>` element; the surrounding text is
rendered as plain text, so markup inside `text` is never interpreted.

## Import

```tsx
import { Highlight } from '@koobiq/react-components';
```

## Usage

Pass the full string to `text` and the search term to `query`.
When `query` is empty, the text is rendered without highlighting.

<Story of={Stories.Base} />

## Props

<Props of={Stories.Base} />

## Variant

The `variant` prop selects the marking style.

- `background` — a solid background behind the match. This is the default.
Use it in dropdown pickers with filtering and to mark matches in longer
content: descriptions, messages, articles. The solid background stays legible
in hover and selected states inside overlays.
- `bold` — the match is set in a bolder weight, with no background.
This style is unobtrusive and suits quick scanning of familiar data.

<Story of={Stories.Variant} />

## Root tag

The component renders an inline `<span>` by default. Use the `as` prop when the
highlighted text is a block of its own, or when it has to be a specific element
such as a table cell.

<Story of={Stories.RootTag} />

## In text

<Story of={Stories.Text} />

## In autocomplete

`Highlight` marks matches; it does not filter. Filtering stays the consumer's
job — `useFilter` from the library provides locale-aware `contains` and
`startsWith` matchers for that. Pass `textValue` on the option so the collection
keeps a plain-text label for typeahead and accessibility.

<Story of={Stories.InAutocomplete} />

## In search results

Highlighting the query in both the title and the snippet makes it obvious which
part of a result matched.

<Story of={Stories.SearchResults} />

## In a table

<Story of={Stories.InTable} />

## CSS Variables

Use CSS variables to customize the marked fragment.

| Variable |
| ---------------------------------- |
| `--kbq-highlight-color` |
| `--kbq-highlight-background-color` |
| `--kbq-highlight-border-radius` |
| `--kbq-highlight-font-weight` |
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.base {
--highlight-color: ;
--highlight-background-color: ;
--highlight-border-radius: ;
--highlight-font-weight: ;

/* Kept even when a variant leaves the token empty: the declaration still wins
the cascade and then computes to `unset`, which is what neutralises the UA
`mark { background-color: Mark; color: Marktext }` yellow. */
.mark {
color: var(--kbq-highlight-color, var(--highlight-color));
background-color: var(
--kbq-highlight-background-color,
var(--highlight-background-color)
);
border-radius: var(
--kbq-highlight-border-radius,
var(--highlight-border-radius)
);
font-weight: var(--kbq-highlight-font-weight, var(--highlight-font-weight));
}
}

.background {
--highlight-color: var(--kbq-foreground-contrast);
--highlight-background-color: var(--kbq-background-highlight);
--highlight-border-radius: var(--kbq-size-xxs);
}

.bold {
--highlight-font-weight: var(--kbq-typography-text-normal-strong-font-weight);
}
Loading
Loading