Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2524b7c
feat: Add Tag component for React Native
Apr 8, 2026
31473dc
chore: resolve linting errors
Apr 8, 2026
4b4bc84
chore: fix prettier issues in readme
Apr 8, 2026
55a1aac
chore: add tests for coerage
Apr 8, 2026
98b6cd0
Merge branch 'main' into feat-tag/react-native
amandaye0h Apr 16, 2026
338b885
refactor: move Tag const assertion to shared types (ADR-0004)
amandaye0h Apr 16, 2026
087b91e
refactor: refine layout to inline styles and remove redundant Tailwin…
amandaye0h Apr 16, 2026
e561a9d
chore: update prop variant > severity to align with BannerAlert and I…
amandaye0h Apr 16, 2026
0aca470
refactor: rerender text to use BoxRow, remove label and textProps
amandaye0h Apr 16, 2026
6e60b43
chore: migrate start and endAccessory to shared props for consistency
amandaye0h Apr 16, 2026
6ee5cf7
chore: rename all TagVariant references to TagSeverity for consistency
amandaye0h Apr 16, 2026
55754c2
chore: align export styles
amandaye0h Apr 16, 2026
3b6ee42
chore: align figma <> code
amandaye0h Apr 16, 2026
5399006
chore: update storybook to add default story
amandaye0h Apr 16, 2026
38d1a14
chore: resolve linting issues
amandaye0h Apr 16, 2026
5ac9e7a
chore: update each file to match cursor rules
amandaye0h Apr 16, 2026
6d575c8
chore: fix linting issues in readme
amandaye0h Apr 16, 2026
a409bac
chore: fix inline style issues
amandaye0h Apr 17, 2026
cdbff40
Merge remote-tracking branch 'origin/main' into feat-tag/react-native
amandaye0h Apr 21, 2026
7dd19be
chore: update import paths to shared folder
amandaye0h Apr 21, 2026
aa96b26
chore: remove numeric children handling
amandaye0h Apr 21, 2026
c464af6
chore: update container styling to use tw.style
amandaye0h Apr 21, 2026
7d5d89b
chore: correct box style vs twClassName ordering
amandaye0h Apr 21, 2026
ca8046c
Merge branch 'main' into feat-tag/react-native
georgewrmarshall Apr 21, 2026
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
160 changes: 160 additions & 0 deletions packages/design-system-react-native/src/components/Tag/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Tag

A tag is a compact, non-interactive or interactive label used to categorize, annotate, or highlight metadata. Tags help users quickly scan, filter, and understand content relationships at a glance.

**Figma:** [MMDS Components — Tag](https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-MMDS-Components?node-id=12339-1167)

```tsx
import { Tag } from '@metamask/design-system-react-native';

<Tag>Default Example</Tag>;
```

## Props

### `severity`

Semantic emphasis for background, string child text color, and icons. Values use `TagSeverity` from `@metamask/design-system-shared` (same pattern as `BannerAlert` / `IconAlert`).

Available values:

- `TagSeverity.Neutral`
- `TagSeverity.Success`
- `TagSeverity.Error`
- `TagSeverity.Warning`
- `TagSeverity.Info`

| TYPE | REQUIRED | DEFAULT |
| -------------- | -------- | --------------------- |
| `TagSeverity?` | No | `TagSeverity.Neutral` |

```tsx
import { Tag } from '@metamask/design-system-react-native';
import { TagSeverity } from '@metamask/design-system-shared';

<Tag severity={TagSeverity.Success}>Success</Tag>
<Tag severity={TagSeverity.Error}>Error</Tag>
```

### `children`

Main content. String children are rendered with design-system `Text` (`BodyXs`, medium weight, severity-based color). Other React nodes are rendered as-is (use your own `Text` or layout inside when needed).

| TYPE | REQUIRED | DEFAULT |
| ----------- | -------- | ----------- |
| `ReactNode` | No | `undefined` |

```tsx
import { Tag } from '@metamask/design-system-react-native';
import { Text } from 'react-native';

<Tag>
<Text>Custom children content</Text>
</Tag>;
```

### `startIconName` / `endIconName`

Optional `IconName` for small icons at the start or end of the tag (`IconSize.Xs` by default). Prefer these when using built-in icons; use `startIconProps` / `endIconProps` for overrides (including `name` instead of `startIconName` / `endIconName`).

| TYPE | REQUIRED | DEFAULT |
| ----------- | -------- | ----------- |
| `IconName?` | No | `undefined` |

```tsx
import { IconName, Tag } from '@metamask/design-system-react-native';

<Tag startIconName={IconName.Warning}>With start icon</Tag>
<Tag endIconName={IconName.ArrowRight}>With end icon</Tag>
```

### `startIconProps` / `endIconProps`

Optional partial `IconProps` passed through to the underlying `Icon`. You may set `name` here instead of `startIconName` / `endIconName`.

| TYPE | REQUIRED | DEFAULT |
| --------------------- | -------- | ----------- |
| `Partial<IconProps>?` | No | `undefined` |

### `startAccessory` / `endAccessory`

Optional React nodes shown when no start/end icon is resolved (e.g. custom glyph or badge). Icons take precedence when `startIconName` / `endIconName` (or `name` in icon props) is set.

| TYPE | REQUIRED | DEFAULT |
| ----------- | -------- | ----------- |
| `ReactNode` | No | `undefined` |

```tsx
import { Tag } from '@metamask/design-system-react-native';
import { Text } from 'react-native';

<Tag startAccessory={<Text>→</Text>}>With accessory</Tag>;
```

### `testID`

Test identifier for selecting the component in tests.

| TYPE | REQUIRED | DEFAULT |
| -------- | -------- | ----------- |
| `string` | No | `undefined` |

### `twClassName`

Use the `twClassName` prop to add Tailwind CSS classes to the component. These classes will be merged with the component's default classes using `twMerge`, allowing you to:

- Add new styles that don't exist in the default component
- Override the component's default styles when needed

| TYPE | REQUIRED | DEFAULT |
| -------- | -------- | ----------- |
| `string` | No | `undefined` |

```tsx
import { Tag } from '@metamask/design-system-react-native';
import { Text } from 'react-native';

// Add additional styles
<Tag twClassName="mt-4">
<Text>Custom Background</Text>
</Tag>

// Override default styles
<Tag twClassName="bg-error-default">
<Text>Override Background</Text>
</Tag>
```

### `style`

Use the `style` prop to customize the component's appearance with React Native styles. For consistent styling, prefer using `twClassName` with Tailwind classes when possible. Use `style` with `tw.style()` for conditionals or dynamic values.

| TYPE | REQUIRED | DEFAULT |
| ---------------------- | -------- | ----------- |
| `StyleProp<ViewStyle>` | No | `undefined` |

```tsx
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import { Tag } from '@metamask/design-system-react-native';
import { Text } from 'react-native';

export const ConditionalExample = ({ isActive }: { isActive: boolean }) => {
const tw = useTailwind();

return (
<Tag style={tw.style('bg-default', isActive && 'bg-success-default')}>
<Text>Conditional styling</Text>
</Tag>
);
};
```

## Accessibility

- String `children` are rendered with design-system typography inside the layout row, so assistive technologies can treat visible label text like normal copy. Prefer clear, concise labels; do not rely on color or icons alone to convey meaning.
- Icons are decorative unless your app assigns accessibility labels on the underlying `Icon` via `startIconProps` / `endIconProps` when needed.
- `testID` is intended for automated tests only; it does not replace accessible names for users.

## References

[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TagSeverity, TextColor } from '@metamask/design-system-shared';

import { BoxBackgroundColor, IconColor } from '../../types';

export const MAP_TAG_SEVERITY_BACKGROUND: Record<
TagSeverity,
BoxBackgroundColor
> = {
[TagSeverity.Neutral]: BoxBackgroundColor.BackgroundMuted,
[TagSeverity.Success]: BoxBackgroundColor.SuccessMuted,
[TagSeverity.Error]: BoxBackgroundColor.ErrorMuted,
[TagSeverity.Warning]: BoxBackgroundColor.WarningMuted,
[TagSeverity.Info]: BoxBackgroundColor.InfoMuted,
};

export const MAP_TAG_SEVERITY_TEXT_COLOR: Record<TagSeverity, TextColor> = {
[TagSeverity.Neutral]: TextColor.TextDefault,
[TagSeverity.Success]: TextColor.SuccessDefault,
[TagSeverity.Error]: TextColor.ErrorDefault,
[TagSeverity.Warning]: TextColor.WarningDefault,
[TagSeverity.Info]: TextColor.InfoDefault,
};

export const MAP_TAG_SEVERITY_ICON_COLOR: Record<TagSeverity, IconColor> = {
[TagSeverity.Neutral]: IconColor.IconDefault,
[TagSeverity.Success]: IconColor.SuccessDefault,
[TagSeverity.Error]: IconColor.ErrorDefault,
[TagSeverity.Warning]: IconColor.WarningDefault,
[TagSeverity.Info]: IconColor.InfoDefault,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// import figma needs to remain as figma otherwise it breaks code connect
// eslint-disable-next-line import-x/no-named-as-default
import figma from '@figma/code-connect';
import { TagSeverity } from '@metamask/design-system-shared';
import React from 'react';

import { IconName } from '../Icon';

import { Tag } from './Tag';

/**
* -- This file was auto-generated by Code Connect --
* React Native implementation of Tag (`figma.connect` for Figma Dev Mode).
*
* [MMDS Tag in Figma](https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-MMDS-Components?node-id=12339-6553)
*
* Root Figma props: `severity`, `state`, `icons`. The first argument to each `figma.*`
* helper must match Dev Mode property names exactly.
*
* - **`state`** (default / hover / pressed) is visual-only in Figma; `Tag` has no matching prop yet.
* - **`icons`** drives `startIconName` / `endIconName` with a placeholder icon (`IconName.Tag`).
* - Label copy comes from the nested **Label** instance (`Label text`), same pattern as `Checkbox.figma.tsx`.
*/
figma.connect(
Tag,
'https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-MMDS-Components?node-id=12339-6553',
Copy link
Copy Markdown
Contributor Author

@amandaye0h amandaye0h Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this so there is better connection between code:design. It'll help with maintainability in the long term. I can remove it if this is premature at this stage of our DS.

If/when this PR is approved, I'll need to add the code link in Figma.

{
props: {
severity: figma.enum('severity', {
neutral: TagSeverity.Neutral,
error: TagSeverity.Error,
info: TagSeverity.Info,
success: TagSeverity.Success,
warning: TagSeverity.Warning,
}),
state: figma.enum('state', {
default: 'default',
hover: 'hover',
pressed: 'pressed',
}),
Comment thread
cursor[bot] marked this conversation as resolved.
icons: figma.enum('icons', {
none: 'none',
start: 'start',
'start & end': 'both',
}),
label: figma.nestedProps('Label', {
text: figma.string('Label text'),
}),
},
example: ({ severity, state: _figmaState, icons, label }) => {
const startIconName =
icons === 'start' || icons === 'both' ? IconName.Tag : undefined;
const endIconName = icons === 'both' ? IconName.Tag : undefined;

return (
<Tag
severity={severity}
startIconName={startIconName}
endIconName={endIconName}
>
{label.text}
</Tag>
);
},
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { TagSeverity } from '@metamask/design-system-shared';
import type { Meta, StoryObj } from '@storybook/react-native';
import React from 'react';
import { Text } from 'react-native';

import { IconName } from '../Icon';

import { Tag } from './Tag';
import type { TagProps } from './Tag.types';

const meta: Meta<TagProps> = {
title: 'Components/Tag',
component: Tag,
parameters: {},
argTypes: {
severity: {
control: 'select',
options: Object.values(TagSeverity),
},
children: {
control: 'text',
},
startIconName: {
control: 'select',
options: Object.values(IconName),
},
endIconName: {
control: 'select',
options: Object.values(IconName),
},
},
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our component documentation standards, the first story must always be Default with minimal args and all controls wired up via argTypes. This powers the Storybook controls panel.

const meta: Meta<typeof Tag> = {
  title: 'Components/Tag',
  component: Tag,
  argTypes: {
    severity: {
      control: 'select',
      options: Object.values(TagSeverity),
    },
    startIconName: { control: 'select', options: Object.values(IconName) },
    endIconName: { control: 'select', options: Object.values(IconName) },
  },
};

export const Default: Story = {
  args: {
    children: 'Tag',
    severity: TagSeverity.Neutral,
  },
};

non-blocking: TagVariant is imported from './Tag.constants' — once moved to shared, import from '.' (the index) to match the pattern used across the codebase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


export default meta;
type Story = StoryObj<TagProps>;

export const Default: Story = {
args: {
children: 'Tag',
severity: TagSeverity.Neutral,
},
};

export const Severity: Story = {
render: () => (
<>
<Tag severity={TagSeverity.Neutral}>Neutral</Tag>
<Tag severity={TagSeverity.Success} twClassName="mt-2">
Success
</Tag>
<Tag severity={TagSeverity.Error} twClassName="mt-2">
Error
</Tag>
<Tag severity={TagSeverity.Warning} twClassName="mt-2">
Warning
</Tag>
<Tag severity={TagSeverity.Info} twClassName="mt-2">
Info
</Tag>
</>
),
};

export const StartIconName: Story = {
render: () => <Tag startIconName={IconName.Warning}>Tag</Tag>,
};

export const EndIconName: Story = {
render: () => <Tag endIconName={IconName.ArrowRight}>Tag</Tag>,
};

export const StartAccessory: Story = {
render: () => (
<Tag startAccessory={<Text testID="tag-story-start-accessory">→</Text>}>
Tag
</Tag>
),
};

export const EndAccessory: Story = {
render: () => (
<Tag endAccessory={<Text testID="tag-story-end-accessory">←</Text>}>
Tag
</Tag>
),
};

export const StartAndEndIconNames: Story = {
render: () => (
<Tag startIconName={IconName.Warning} endIconName={IconName.ArrowRight}>
Tag
</Tag>
),
};
Loading
Loading