diff --git a/apps/website/screens/components/chip/code/ChipCodePage.tsx b/apps/website/screens/components/chip/code/ChipCodePage.tsx
index 9f25f914f..41151ff8b 100644
--- a/apps/website/screens/components/chip/code/ChipCodePage.tsx
+++ b/apps/website/screens/components/chip/code/ChipCodePage.tsx
@@ -4,7 +4,23 @@ import DocFooter from "@/common/DocFooter";
import Example from "@/common/example/Example";
import basicUsage from "./examples/basicUsage";
import icons from "./examples/icons";
-import Code, { TableCode } from "@/common/Code";
+import Code, { ExtendedTableCode, TableCode } from "@/common/Code";
+
+const avatarTypeString = `{
+ color?: 'primary' | 'secondary' | 'tertiary' |
+ 'success' | 'info' | 'neutral' | 'warning' | 'error';
+ disabled?: boolean;
+ icon?: string | SVG;
+ imgSrc?: string;
+ label?: string;
+ linkHref?: string;
+ onClick??: () => void;
+ shape?: 'circle' | 'square';
+ size?: 'xsmall' | 'small' | 'medium' |
+ 'large' | 'xlarge' | 'xxlarge';
+ tabIndex?: number';
+ title?: string;
+}`;
const sections = [
{
@@ -20,6 +36,19 @@ const sections = [
+
+ | avatar |
+
+ {avatarTypeString}
+ |
+
+ Avatar that will be placed before the chip label only when the chip size is 'medium' or{" "}
+ 'large'.
+ |
+
+ false
+ |
+
| disabled |
@@ -80,9 +109,9 @@ const sections = [
Material Symbol
{" "}
- name or SVG element as the icon that will be placed before the chip label. When using Material Symbols,
- replace spaces with underscores. By default they are outlined if you want it to be filled prefix the
- symbol name with "filled_".
+ name or SVG element as the icon that will be placed before the chip label when an avatar is
+ not provided. When using Material Symbols, replace spaces with underscores. By default they are outlined
+ if you want it to be filled prefix the symbol name with "filled_".
|
- |
diff --git a/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx b/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx
index 406df4d57..aba3f28d4 100644
--- a/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx
+++ b/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx
@@ -28,16 +28,72 @@ const sections = [
- Prefix (Optional): the prefix can be an icon or an action icon that provides
- additional context or functionality.
+ Container (Required):
+
+ The outer wrapper of the chip.
+
+ Defines:
+
+ Overall size (Small / Medium / Large)
+
+ Interactive area (default, focus, hover, active and disabled)
+
+
+
+
+ Acts as the main clickable surface when no action icon is present.
+
+
- Label: the primary text that conveys the chip's meaning, such as a tag name or a selected
- option. It should be concise, clear, and relevant to the chip's function.
+ Left Element (Optional): Supported types:
+
+
+ Icon
+
+ Allowed in Small, Medium and Large.
+ Used for status, category, or action hint.
+
+
+
+
+ Avatar
+
+ Allowed only in Medium and Large.
+ Represents people, entities or profiles.
+
+
+
+ Small size supports icons only to preserve compactness and clarity.
- Suffix (Optional): the suffix can be an icon or an action icon that enhances
- interactivity.
+ Label (Required)
+
+ Text content displayed inside the chip
+
+ Characteristics:
+
+ Short, concise text
+ Single-line only(no-wrapping)
+ Truncated when exceeding maximum width
+ Tooltip appears on hover/focus when truncated
+
+
+ Serves as the primary identifier of the chip.
+
+
+
+ Action Icon (Optional) - Appears at the end of the chip. Common usage:
+
+ Remove / clear action (✕)
+ Secondary inline action (if applicable)
+
+ Behavior:
+
+ Has its own interaction target
+ Does not trigger the main chip action
+ Disabled when the chip is disabled
+
>
diff --git a/packages/lib/src/action-icon/ActionIcon.tsx b/packages/lib/src/action-icon/ActionIcon.tsx
index b4b521dce..11330e460 100644
--- a/packages/lib/src/action-icon/ActionIcon.tsx
+++ b/packages/lib/src/action-icon/ActionIcon.tsx
@@ -20,6 +20,7 @@ const ActionIconContainer = styled.div<
hasAction?: boolean;
size: ActionIconPropTypes["size"];
disabled?: ActionIconPropTypes["disabled"];
+ isAvatar?: boolean;
} & React.AnchorHTMLAttributes
>`
position: relative;
@@ -40,15 +41,12 @@ const ActionIconContainer = styled.div<
color: inherit;
outline: none;
}
- ${({ hasAction, disabled, size }) =>
+
+ ${({ hasAction, disabled, size, isAvatar }) =>
!disabled &&
hasAction &&
css`
cursor: pointer;
- &:hover > div:first-child > div:first-child,
- &:active > div:first-child > div:first-child {
- display: block;
- }
&:focus:enabled > div:first-child,
&:active:enabled > div:first-child {
outline-style: solid;
@@ -59,15 +57,37 @@ const ActionIconContainer = styled.div<
&:focus-visible:enabled {
outline: none;
}
+ ${isAvatar
+ ? css`
+ &:hover > div:first-child > div:first-child,
+ &:active > div:first-child > div:first-child {
+ display: block;
+ }
+ `
+ : css`
+ &:hover > div:first-child,
+ &:active > div:first-child {
+ background-color: var(--color-bg-alpha-light);
+ }
+ `}
`}
- ${({ disabled }) =>
+
+ ${({ disabled, isAvatar }) =>
disabled &&
css`
cursor: not-allowed;
- & > div:first-child > div:first-child {
- display: block;
- background-color: rgba(255, 255, 255, 0.5);
- }
+ ${isAvatar
+ ? css`
+ & > div:first-child > div:first-child {
+ display: block;
+ background-color: rgba(255, 255, 255, 0.5);
+ }
+ `
+ : css`
+ & > div:first-child > div:first-child {
+ color: var(--color-fg-neutral-medium);
+ }
+ `}
`}
`;
@@ -155,9 +175,10 @@ const ForwardedActionIcon = forwardRef(
aria-label={(onClick || linkHref) && (ariaLabel || title || "Action Icon")}
disabled={disabled}
ref={ref}
+ isAvatar={color !== "transparent"}
>
- {(!!onClick || !!linkHref) && }
+ {color !== "transparent" && (!!onClick || !!linkHref || disabled) && }
{content ? (
content
) : (
diff --git a/packages/lib/src/chip/Chip.stories.tsx b/packages/lib/src/chip/Chip.stories.tsx
index 12e6e9a49..bee450f3f 100644
--- a/packages/lib/src/chip/Chip.stories.tsx
+++ b/packages/lib/src/chip/Chip.stories.tsx
@@ -2,11 +2,25 @@ import Title from "../../.storybook/components/Title";
import ExampleContainer from "../../.storybook/components/ExampleContainer";
import DxcChip from "./Chip";
import { Meta, StoryObj } from "@storybook/react-vite";
-import { userEvent } from "storybook/internal/test";
+import { useEffect } from "react";
export default {
title: "Chip",
component: DxcChip,
+ decorators: [
+ (Story) => {
+ useEffect(() => {
+ const prev = document.body.style.cssText;
+ document.body.style.backgroundColor = "var(--color-bg-neutral-light)";
+ document.body.style.padding = "0";
+ return () => {
+ document.body.style.cssText = prev;
+ };
+ }, []);
+
+ return ;
+ },
+ ],
} satisfies Meta;
const iconSVG = (
@@ -42,57 +56,73 @@ const Chip = () => (
<>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+ console.log("action clicked") }} />
-
-
+
+ console.log("action clicked") }}
+ />
-
-
-
+
-
-
-
-
+
+ console.log("action clicked") }}
+ label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasd fgsss"
+ />
-
-
-
-
-
-
-
-
-
+
+
+
+
+ console.log("action clicked") }}
+ label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasdf"
+ />
@@ -126,18 +156,34 @@ const Chip = () => (
>
);
-const ChipPrefixFocused = () => (
-
-
- {}} />
-
-);
-
-const ChipSuffixFocused = () => (
-
-
- {}} />
-
+const ChipActionStates = () => (
+ <>
+
+
+ {} }} prefix={{ color: "primary" }} />
+
+
+
+ {} }} prefix={{ color: "primary" }} />
+
+
+
+ {} }} prefix={{ color: "primary" }} />
+
+
+
+ {} }} prefix={{ color: "primary" }} />
+
+
+
+ {} }}
+ prefix={{ color: "primary" }}
+ disabled
+ />
+
+ >
);
type Story = StoryObj;
@@ -146,16 +192,6 @@ export const Chromatic: Story = {
render: Chip,
};
-export const PrefixFocused: Story = {
- render: ChipPrefixFocused,
- play: async () => {
- await userEvent.tab();
- },
-};
-
-export const SuffixFocused: Story = {
- render: ChipSuffixFocused,
- play: async () => {
- await userEvent.tab();
- },
+export const ActionStates: Story = {
+ render: ChipActionStates,
};
diff --git a/packages/lib/src/chip/Chip.test.tsx b/packages/lib/src/chip/Chip.test.tsx
index e32448734..c81230be5 100644
--- a/packages/lib/src/chip/Chip.test.tsx
+++ b/packages/lib/src/chip/Chip.test.tsx
@@ -6,16 +6,24 @@ describe("Chip component tests", () => {
const { getByText } = render();
expect(getByText("Chip")).toBeTruthy();
});
- test("Calls correct function when clicking on prefix icon", () => {
- const onClick = jest.fn();
- const { getByText, getByRole } = render();
- expect(getByText("Chip")).toBeTruthy();
- fireEvent.click(getByRole("button"));
- expect(onClick).toHaveBeenCalled();
+ test("Chip renders correctly with prefix icon", () => {
+ const { getByRole } = render();
+ const avatar = getByRole("img", { hidden: true });
+ expect(avatar).toBeTruthy();
+ });
+ test("Chip renders correctly with avatar", () => {
+ const { getByRole } = render();
+ const avatar = getByRole("img", { hidden: true });
+ expect(avatar).toBeTruthy();
+ });
+ test("Chip doesn't render avatar when size is small", () => {
+ const { queryByRole } = render();
+ const avatar = queryByRole("img", { hidden: true });
+ expect(avatar).not.toBeTruthy();
});
- test("Calls correct function when clicking on suffix icon", () => {
+ test("Calls correct function when clicking on action icon", () => {
const onClick = jest.fn();
- const { getByText, getByRole } = render();
+ const { getByText, getByRole } = render();
expect(getByText("Chip")).toBeTruthy();
fireEvent.click(getByRole("button"));
expect(onClick).toHaveBeenCalled();
diff --git a/packages/lib/src/chip/Chip.tsx b/packages/lib/src/chip/Chip.tsx
index 2e83c1836..56aa3413a 100644
--- a/packages/lib/src/chip/Chip.tsx
+++ b/packages/lib/src/chip/Chip.tsx
@@ -1,23 +1,29 @@
import styled from "@emotion/styled";
-import { getMargin } from "../common/utils";
import { spaces } from "../common/variables";
import DxcIcon from "../icon/Icon";
-import ChipPropsType from "./types";
+import ChipPropsType, { ChipAvatarType } from "./types";
import DxcActionIcon from "../action-icon/ActionIcon";
+import DxcAvatar from "../avatar/Avatar";
+import { isValidElement } from "react";
+import { Tooltip } from "../tooltip/Tooltip";
-const calculateWidth = (margin: ChipPropsType["margin"]) =>
- `calc(100% - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`;
-
-const Chip = styled.div<{ margin: ChipPropsType["margin"]; disabled: ChipPropsType["disabled"] }>`
+const Chip = styled.div<{
+ margin: ChipPropsType["margin"];
+ size: ChipPropsType["size"];
+ disabled: ChipPropsType["disabled"];
+}>`
+ height: ${({ size }) =>
+ size === "small" ? "var(--height-s)" : size === "large" ? "var(--height-xl)" : "var(--height-m)"};
+ min-width: ${({ size }) => (size === "small" ? "60px" : "80px")};
+ max-width: 172px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
- gap: var(--spacing-gap-s);
- min-height: var(--height-xl);
- max-width: ${(props) => calculateWidth(props.margin)};
- background-color: var(--color-bg-neutral-light);
+ justify-content: center;
+ gap: var(--spacing-gap-xs);
+ background-color: var(--color-bg-primary-lightest);
border-radius: var(--border-radius-xl);
- padding: var(--spacing-padding-none) var(--spacing-padding-m);
+ padding: ${({ size }) => (size === "small" ? "var(--spacing-padding-xxs)" : "var(--spacing-padding-xs)")};
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
margin-top: ${(props) =>
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
@@ -31,10 +37,10 @@ const Chip = styled.div<{ margin: ChipPropsType["margin"]; disabled: ChipPropsTy
`;
const LabelContainer = styled.span<{ disabled: ChipPropsType["disabled"] }>`
- font-size: var(--typography-label-l);
+ font-size: var(--typography-label-s);
font-family: var(--typography-font-family);
font-weight: var(--typography-label-regular);
- color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")};
+ color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-lightest)" : "var(--color-fg-neutral-dark)")};
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@@ -44,58 +50,56 @@ const IconContainer = styled.div<{
disabled: ChipPropsType["disabled"];
}>`
display: flex;
- border-radius: var(--border-radius-xs);
- color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")};
- font-size: var(--height-s);
+ align-items: center;
+ justify-content: center;
+ color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")};
+ font-size: var(--height-xxs);
svg {
- width: 24px;
- height: var(--height-s);
+ height: var(--height-xxs);
+ width: var(--height-xxs);
}
`;
-const DxcChip = ({
- label,
- suffixIcon,
- prefixIcon,
- onClickSuffix,
- onClickPrefix,
- disabled,
- margin,
- tabIndex = 0,
-}: ChipPropsType) => (
-
- {prefixIcon &&
- (typeof onClickPrefix === "function" ? (
-
- ) : (
-
- {typeof prefixIcon === "string" ? : prefixIcon}
-
- ))}
- {label && {label}}
- {suffixIcon &&
- (typeof onClickSuffix === "function" ? (
-
- ) : (
-
- {typeof suffixIcon === "string" ? : suffixIcon}
-
- ))}
-
-);
+const DxcChip = ({ action, disabled = false, label, margin, prefix, size = "medium", tabIndex = 0 }: ChipPropsType) => {
+ const isAvatarPrefix = (prefix: ChipPropsType["prefix"]): prefix is ChipAvatarType =>
+ typeof prefix === "object" && prefix !== null && "color" in prefix;
+
+ return (
+ 14 ? label : undefined}>
+
+ {prefix &&
+ (isAvatarPrefix(prefix) && size !== "small" ? (
+
+ ) : typeof prefix === "string" ? (
+
+
+
+ ) : (
+ isValidElement(prefix) && {prefix}
+ ))}
+
+ {label && {label}}
+
+ {action && (
+
+ )}
+
+
+ );
+};
export default DxcChip;
diff --git a/packages/lib/src/chip/types.ts b/packages/lib/src/chip/types.ts
index 004385e1c..1fb48660c 100644
--- a/packages/lib/src/chip/types.ts
+++ b/packages/lib/src/chip/types.ts
@@ -1,35 +1,57 @@
import { Margin, SVG, Space } from "../common/utils";
+import AvatarProps from "../avatar/types";
-type Props = {
- /**
- * Text to be placed on the chip.
- */
- label?: string;
+type Size = "small" | "medium" | "large";
+export type ChipAvatarType = {
+ color?: AvatarProps["color"];
+ profileName?: AvatarProps["label"];
+ imageSrc?: AvatarProps["imageSrc"];
+ icon?: AvatarProps["icon"];
+};
+type Action = {
/**
- * Element or path used as icon to be placed after the chip label.
+ * Icon to be placed in the action.
*/
- suffixIcon?: string | SVG;
+ icon: string | SVG;
/**
- * Element or path used as icon to be placed before the chip label.
+ * This function will be called when the user clicks the action.
*/
- prefixIcon?: string | SVG;
+ onClick: () => void;
/**
- * This function will be called when the suffix is clicked.
+ * Text representing advisory information related
+ * to the button's action. Under the hood, this prop also serves
+ * as an accessible label for the component.
*/
- onClickSuffix?: () => void;
+ title?: string;
+};
+
+type Props = {
/**
- * This function will be called when the prefix is clicked.
+ * Action to be displayed on the right side of the chip after the label.
*/
- onClickPrefix?: () => void;
+ action?: Action;
/**
* If true, the component will be disabled.
*/
disabled?: boolean;
+ /**
+ * Text to be placed on the chip.
+ */
+ label: string;
/**
* Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge').
* You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes.
*/
margin?: Space | Margin;
+ /**
+ * Element, path or avatar used as icon to be placed before the chip label.
+ */
+ prefix?: string | SVG | ChipAvatarType;
+ /**
+ * Size of the component.
+ */
+ size?: Size;
+
/**
* Value of the tabindex attribute.
*/