TagGroup
View .mdContainer for multiple Tag or InteractionTag controls.
Default
Loading interactive example. Full source is available below.
View example source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { TagGroup } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
export const Default = () => {
const styles = /* css */ `
.tag-group-default-example {
display: flex;
flex-direction: column;
row-gap: 10px;
}
`;
return (<>
<style>{styles}</style>
<div className="tag-group-default-example">
Example with Tag:
<TagGroup aria-label="Simple tag group with Tag" role="list">
<Tag role="listitem">Tag 1</Tag>
<Tag role="listitem">Tag 2</Tag>
<Tag role="listitem">Tag 3</Tag>
</TagGroup>
Example with InteractionTag:
<TagGroup aria-label="Simple tag group with InteractionTag">
<InteractionTag>
<InteractionTagPrimary>Tag 1</InteractionTagPrimary>
</InteractionTag>
<InteractionTag>
<InteractionTagPrimary>Tag 2</InteractionTagPrimary>
</InteractionTag>
<InteractionTag>
<InteractionTagPrimary>Tag 3</InteractionTagPrimary>
</InteractionTag>
</TagGroup>
</div>
</>);
};
export default Default;
API referenceGenerated CoreAI prop and type summaries
API
Generated CoreAI prop summaries, not replacement type declarations. Most native DOM/event and ARIA props are omitted. ? means optional; JSDoc shows defaults, deprecations, and conditional requirements. Defaults assume the standard CoreAI provider.
TagGroup API
TagGroup component - a container for multiple tags. Provides context to size or dismiss children tags.
interface TagGroupProps {
appearance?: "brand" | "filled" | "outline";
as?: "div";
children?: any;
className?: string;
/**
* Sets selected values for an uncontrolled component.
*/
defaultSelectedValues?: string[];
/**
* A TagGroup can show that it cannot be interacted with.
* @default false
*/
disabled?: boolean;
dismissible?: boolean;
/**
* Callback for when a tag is dismissed
*/
onDismiss?: TagDismissHandler<string>;
/**
* Callback for when a tag is selected
*/
onTagSelect?: EventHandler<TagSelectData<string>>;
ref?: Ref<HTMLDivElement>;
/**
* Values of the selected tags
*/
selectedValues?: string[];
size?: "small" | "medium" | "extra-small";
style?: CSSProperties;
}
Implementation
CoreAI 0.12.0; Fluent 9.74.9.
Import components from @coreai-ds/react-components. CoreAI guidance above takes precedence over API demonstration examples.
Disabled
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import * as React from 'react';
const useStyles = makeStyles({
container: {
display: 'flex',
flexDirection: 'column',
rowGap: '10px',
},
});
const WithTags = () => (<TagGroup disabled aria-label="Disabled tag group with Tag" role="list">
<Tag role="listitem">Tag 1</Tag>
<Tag role="listitem">Tag 2</Tag>
<Tag role="listitem">Tag 3</Tag>
</TagGroup>);
const WithInteractionTags = () => (<TagGroup disabled aria-label="Disabled tag group with InteractionTag">
<InteractionTag>
<InteractionTagPrimary>Tag 1</InteractionTagPrimary>
</InteractionTag>
<InteractionTag>
<InteractionTagPrimary>Tag 2</InteractionTagPrimary>
</InteractionTag>
<InteractionTag>
<InteractionTagPrimary>Tag 3</InteractionTagPrimary>
</InteractionTag>
</TagGroup>);
export const Disabled = (): JSXElement => {
const styles = useStyles();
return (<div className={styles.container}>
Disabled example with Tag:
<WithTags />
Disabled example with InteractionTag:
<WithInteractionTags />
</div>);
};
export default Disabled;
Dismiss
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TagGroupProps } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { InteractionTagSecondary } from "@coreai-ds/react-components";
const useStyles = makeStyles({
container: {
display: 'flex',
flexDirection: 'column',
rowGap: '10px',
},
tagGroup: {
flexWrap: 'wrap',
},
resetButton: {
width: 'fit-content',
},
});
const initialTags = [
{ value: '1', children: 'Tag 1' },
{ value: '2', children: 'Tag 2' },
{ value: '3', children: 'Tag 3' },
];
/**
* focus management for the reset button
*/
const useResetExample = (visibleTagsLength: number) => {
const resetButtonRef = React.useRef<HTMLButtonElement>(null);
const firstTagRef = React.useRef<HTMLButtonElement>(null);
const prevVisibleTagsLengthRef = React.useRef<number>(visibleTagsLength);
React.useEffect(() => {
if (visibleTagsLength === 0) {
resetButtonRef.current?.focus();
}
else if (prevVisibleTagsLengthRef.current === 0) {
firstTagRef.current?.focus();
}
prevVisibleTagsLengthRef.current = visibleTagsLength;
}, [visibleTagsLength]);
return { firstTagRef, resetButtonRef };
};
const DismissWithTags = () => {
const [visibleTags, setVisibleTags] = React.useState(initialTags);
const removeItem: TagGroupProps['onDismiss'] = (_e, { value }) => {
setVisibleTags([...visibleTags].filter(tag => tag.value !== value));
};
const resetItems = () => setVisibleTags(initialTags);
const { firstTagRef, resetButtonRef } = useResetExample(visibleTags.length);
const styles = useStyles();
return (<>
{visibleTags.length !== 0 && (<TagGroup className={styles.tagGroup} onDismiss={removeItem} aria-label="TagGroup example with dismissible Tags">
{visibleTags.map((tag, index) => (<Tag dismissible dismissIcon={{ 'aria-label': 'remove' }} value={tag.value} key={tag.value} ref={index === 0 ? firstTagRef : null}>
{tag.children}
</Tag>))}
</TagGroup>)}
<Button onClick={resetItems} ref={resetButtonRef} disabled={visibleTags.length === initialTags.length} className={styles.resetButton} size="small">
Reset the example
</Button>
</>);
};
const DismissWithInteractionTags = () => {
const [visibleTags, setVisibleTags] = React.useState(initialTags);
const removeItem: TagGroupProps['onDismiss'] = (_e, { value }) => {
setVisibleTags([...visibleTags].filter(tag => tag.value !== value));
};
const resetItems = () => setVisibleTags(initialTags);
const { firstTagRef, resetButtonRef } = useResetExample(visibleTags.length);
const styles = useStyles();
return (<>
{visibleTags.length !== 0 && (<TagGroup className={styles.tagGroup} onDismiss={removeItem} aria-label="Dismiss example">
{visibleTags.map((tag, index) => (<InteractionTag value={tag.value} key={tag.value}>
<InteractionTagPrimary hasSecondaryAction ref={index === 0 ? firstTagRef : null}>
{tag.children}
</InteractionTagPrimary>
<InteractionTagSecondary aria-label="remove"/>
</InteractionTag>))}
</TagGroup>)}
<Button onClick={resetItems} ref={resetButtonRef} disabled={visibleTags.length !== 0} className={styles.resetButton} size="small">
Reset the example
</Button>
</>);
};
export const Dismiss = (): JSXElement => {
const styles = useStyles();
return (<div className={styles.container}>
Example with Tag:
<DismissWithTags />
Example with InteractionTag:
<DismissWithInteractionTags />
</div>);
};
export default Dismiss;
Select
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { makeResetStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TagValue } from "@coreai-ds/react-components";
import type { TagGroupProps } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { InteractionTagSecondary } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
const useContainerStyles = makeResetStyles({
display: 'flex',
flexDirection: 'column',
rowGap: '10px',
});
const WithMultiselectTags = () => {
const [seletedTags, setSelectedTags] = React.useState<Array<TagValue> | undefined>([]);
const selectItem: TagGroupProps['onTagSelect'] = (_e, { value }) => {
if (!seletedTags) {
return;
}
if (seletedTags.includes(value)) {
setSelectedTags(seletedTags.filter(tag => tag !== value));
}
else {
setSelectedTags([...seletedTags, value]);
}
};
return (<>
<p>Selected values: {seletedTags?.join(', ')}</p>
<TagGroup onTagSelect={selectItem} aria-label="Tag group with Multiselect Tag">
<InteractionTag>
<InteractionTagPrimary>Tag 1</InteractionTagPrimary>
</InteractionTag>
<InteractionTag>
<InteractionTagPrimary>Tag 2</InteractionTagPrimary>
</InteractionTag>
<InteractionTag>
<InteractionTagPrimary>Tag 3</InteractionTagPrimary>
</InteractionTag>
</TagGroup>
</>);
};
const initialTags = [
{ value: '1', children: 'Tag 1' },
{ value: '2', children: 'Tag 2' },
{ value: '3', children: 'Tag 3' },
];
/**
* focus management for the reset button
*/
const useResetExample = (visibleTagsLength: number) => {
const resetButtonRef = React.useRef<HTMLButtonElement>(null);
const firstTagRef = React.useRef<HTMLButtonElement>(null);
const prevVisibleTagsLengthRef = React.useRef<number>(visibleTagsLength);
React.useEffect(() => {
if (visibleTagsLength === 0) {
resetButtonRef.current?.focus();
}
else if (prevVisibleTagsLengthRef.current === 0) {
firstTagRef.current?.focus();
}
prevVisibleTagsLengthRef.current = visibleTagsLength;
}, [visibleTagsLength]);
return { firstTagRef, resetButtonRef };
};
const useStyles = makeStyles({
resetButton: {
width: 'fit-content',
},
});
const DismissWithInteractionTags = () => {
const [visibleTags, setVisibleTags] = React.useState(initialTags);
const [seletedTags, setSelectedTags] = React.useState<Array<TagValue> | undefined>([]);
const removeItem: TagGroupProps['onDismiss'] = (_e, { value }) => {
if (seletedTags?.includes(value)) {
setSelectedTags(seletedTags?.filter(tag => tag !== value));
}
setVisibleTags([...visibleTags].filter(tag => tag.value !== value));
};
const resetItems = () => setVisibleTags(initialTags);
const selectItem: TagGroupProps['onTagSelect'] = (_e, { value }) => {
if (!seletedTags) {
return;
}
if (seletedTags.includes(value)) {
setSelectedTags(seletedTags.filter(tag => tag !== value));
}
else {
setSelectedTags([...seletedTags, value]);
}
};
const { firstTagRef, resetButtonRef } = useResetExample(visibleTags.length);
const styles = useStyles();
return (<>
<p>Selected values: {seletedTags?.join(', ')}</p>
{visibleTags.length !== 0 && (<TagGroup onTagSelect={selectItem} onDismiss={removeItem} aria-label="Tag group with Dismissable Multiselect Tag">
{visibleTags.map((tag, index) => (<InteractionTag value={tag.value} key={tag.value}>
<InteractionTagPrimary hasSecondaryAction ref={index === 0 ? firstTagRef : null}>
{tag.children}
</InteractionTagPrimary>
<InteractionTagSecondary aria-label="remove"/>
</InteractionTag>))}
</TagGroup>)}
<Button onClick={resetItems} ref={resetButtonRef} disabled={visibleTags.length !== 0} className={styles.resetButton} size="small">
Reset the example
</Button>
</>);
};
export const Select = (): JSXElement => {
const containerStyles = useContainerStyles();
return (<div className={containerStyles}>
Example with InteractionTag:
<WithMultiselectTags />
Example with Dismissable InteractionTag:
<DismissWithInteractionTags />
</div>);
};
export default Select;
Sizes
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import type { TagSize } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { Avatar } from "@coreai-ds/react-components";
import { CalendarMonthRegular } from "@fluentui/react-icons";
import { InteractionTagSecondary } from "@coreai-ds/react-components";
import * as React from 'react';
const useContainerStyles = makeStyles({
root: {
display: 'flex',
flexDirection: 'column',
rowGap: '10px',
},
tagGroup: {
flexWrap: 'wrap',
},
});
export const Sizes = (): JSXElement => {
const containerStyles = useContainerStyles();
const sizes: TagSize[] = ['medium', 'small', 'extra-small'];
return (<div className={containerStyles.root}>
{sizes.map(size => (<div key={size}>
{`${size}: `}
<TagGroup className={containerStyles.tagGroup} size={size} aria-label={`${size} tag group example`}>
<InteractionTag>
<InteractionTagPrimary media={<Avatar name="Katri Athokas"/>}>{size}</InteractionTagPrimary>
</InteractionTag>
<InteractionTag shape="circular">
<InteractionTagPrimary icon={<CalendarMonthRegular />}>{size}</InteractionTagPrimary>
</InteractionTag>
<InteractionTag>
<InteractionTagPrimary icon={<CalendarMonthRegular />} hasSecondaryAction>
{size}
</InteractionTagPrimary>
<InteractionTagSecondary aria-label="remove"/>
</InteractionTag>
</TagGroup>
</div>))}
</div>);
};
export default Sizes;
With Overflow
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Overflow } from "@coreai-ds/react-components";
import { TagGroup } from "@coreai-ds/react-components";
import type { InteractionTagPrimaryProps } from "@coreai-ds/react-components";
import { Avatar } from "@coreai-ds/react-components";
import { OverflowItem } from "@coreai-ds/react-components";
import { InteractionTag } from "@coreai-ds/react-components";
import { InteractionTagPrimary } from "@coreai-ds/react-components";
import { useOverflowMenu } from "@coreai-ds/react-components";
import { Menu } from "@coreai-ds/react-components";
import { MenuTrigger } from "@coreai-ds/react-components";
import { MenuPopover } from "@coreai-ds/react-components";
import { MenuList } from "@coreai-ds/react-components";
import type { TagProps } from "@coreai-ds/react-components";
import { useIsOverflowItemVisible } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { tagClassNames } from "@coreai-ds/react-components";
import { shorthands } from "@coreai-ds/react-components";
import { MenuItem } from "@coreai-ds/react-components";
import { Tag } from "@coreai-ds/react-components";
import * as React from 'react';
//----- Stories -----//
const useStyles = makeStyles({
container: {
overflow: 'hidden',
padding: '5px',
zIndex: 0, // stop the browser resize handle from piercing the overflow menu
height: 'fit-content',
minWidth: '150px',
resize: 'horizontal',
width: '100%',
boxSizing: 'border-box',
},
tagGroup: {
display: 'flex', // TagGroup is inline-flex by default, but we want it to be same width as the container
},
});
type DefaultItem = InteractionTagPrimaryProps & {
value: string;
};
const names = [
'Johnie McConnell',
'Allan Munger',
'Erik Nason',
'Kristin Patterson',
'Daisy Phillips',
'Carole Poland',
'Carlos Slattery',
'Robert Tolbert',
'Kevin Sturgis',
'Charlotte Waltson',
'Elliot Woodward',
];
const defaultItems: DefaultItem[] = names.map(name => ({
value: name.replace(' ', '_'),
children: name,
media: (<Avatar aria-hidden="true" // use aria-hidden because InteractionTag contains information in the avatar
name={name} badge={{
status: 'available',
}}/>),
secondaryText: 'Available',
}));
//----- OverflowMenuItem -----//
type OverflowMenuItemProps = {
tag: TagProps;
};
const useMenuItemStyles = makeStyles({
menuItem: {
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalXS}`,
':hover': {
[`& .${tagClassNames.root}`]: {
color: tokens.colorNeutralForeground2Hover,
},
},
},
tag: {
backgroundColor: 'transparent',
...shorthands.borderColor('transparent'),
},
});
/**
* A menu item for an overflow menu that only displays when the tab is not visible
*/
const OverflowMenuItem = (props: OverflowMenuItemProps) => {
const { tag } = props;
const isVisible = useIsOverflowItemVisible(tag.value!);
const styles = useMenuItemStyles();
if (isVisible) {
return null;
}
return (<MenuItem key={tag.value} className={styles.menuItem}>
<Tag {...tag} as="span" className={styles.tag}/>
</MenuItem>);
};
//----- OverflowMenu -----//
/**
* A menu for viewing tags that have overflowed and are not visible.
*/
const OverflowMenu = () => {
const { ref, isOverflowing, overflowCount } = useOverflowMenu<HTMLButtonElement>();
if (!isOverflowing) {
return null;
}
return (<InteractionTag>
<Menu>
<MenuTrigger disableButtonEnhancement>
<InteractionTagPrimary ref={ref} aria-label={`${overflowCount} more tags`}>
{`+${overflowCount}`}
</InteractionTagPrimary>
</MenuTrigger>
<MenuPopover>
<MenuList>
{defaultItems.map(item => (<OverflowMenuItem key={item.value} tag={item}/>))}
</MenuList>
</MenuPopover>
</Menu>
</InteractionTag>);
};
export const WithOverflow = (): JSXElement => {
const styles = useStyles();
return (<div className={styles.container}>
<Overflow minimumVisible={2} padding={60}>
<TagGroup className={styles.tagGroup} aria-label="Overflow example">
{defaultItems.map(({ value, ...rest }) => (<OverflowItem key={value} id={value!}>
<InteractionTag key={value}>
<InteractionTagPrimary {...rest}/>
</InteractionTag>
</OverflowItem>))}
<OverflowMenu />
</TagGroup>
</Overflow>
</div>);
};
export default WithOverflow;